Stefan McShane пре 3 година
родитељ
комит
52227c5a17

+ 1 - 1
.github/workflows/build-dev-cli.yaml

@@ -1,4 +1,4 @@
-name: Deploy to dev
+name: Build Dev CLI
 on:
   push:
     branches:

+ 21 - 0
api/client/stack.go

@@ -45,3 +45,24 @@ func (c *Client) CreatePorterApp(
 
 	return resp, err
 }
+
+// CreateOrUpdatePorterAppEvent will create a porter app event if one does not exist, or else it will update the existing one if an ID is passed in the object
+func (c *Client) CreateOrUpdatePorterAppEvent(
+	ctx context.Context,
+	projectID, clusterID uint,
+	name string,
+	req *types.CreateOrUpdatePorterAppEventRequest,
+) (types.PorterAppEvent, error) {
+	resp := &types.PorterAppEvent{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/stacks/%s/events",
+			projectID, clusterID, name,
+		),
+		req,
+		resp,
+	)
+
+	return *resp, err
+}

+ 2 - 2
api/server/handlers/stacks/create_porter_app.go

@@ -183,7 +183,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		// create the app chart
 		_, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("error deploying app: %s", err.Error()), http.StatusBadRequest))
 
 			_, err = helmAgent.UninstallChart(ctx, stackName)
 			if err != nil {
@@ -287,7 +287,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		// update the chart
 		_, err = helmAgent.UpgradeInstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deploying app: %s", err.Error())))
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("error deploying app: %s", err.Error()), http.StatusBadRequest))
 			return
 		}
 

+ 105 - 0
api/server/handlers/stacks/create_porter_app_events.go

@@ -0,0 +1,105 @@
+package stacks
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/google/uuid"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+type CreateUpdatePorterAppEventHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewCreateUpdatePorterAppEventHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateUpdatePorterAppEventHandler {
+	return &CreateUpdatePorterAppEventHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *CreateUpdatePorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-post-porter-app-event")
+	defer span.End()
+
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "cluster-id", Value: int(cluster.ID)},
+		telemetry.AttributeKV{Key: "project-id", Value: int(cluster.ProjectID)},
+	)
+
+	request := &types.CreateOrUpdatePorterAppEventRequest{}
+	if ok := p.DecodeAndValidate(w, r, request); !ok {
+		e := telemetry.Error(ctx, span, nil, "error decoding request")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	if reqErr != nil {
+		e := telemetry.Error(ctx, span, nil, "error parsing stack name from url")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	event, err := p.createNewAppEvent(ctx, *cluster, stackName, request.Status, string(request.Type), request.TypeExternalSource, request.Metadata)
+	if err != nil {
+		e := telemetry.Error(ctx, span, nil, "error creating new app event")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+	p.WriteResult(w, r, event)
+	return
+}
+
+func (p *CreateUpdatePorterAppEventHandler) createNewAppEvent(ctx context.Context, cluster models.Cluster, stackName string, status string, eventType string, externalSource string, requestMetadata map[string]any) (types.PorterAppEvent, error) {
+	ctx, span := telemetry.NewSpan(ctx, "create-porter-app-event")
+	defer span.End()
+
+	app, err := p.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+	if err != nil {
+		return types.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "error retrieving porter app by name for cluster")
+	}
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "stack-app-name", Value: stackName},
+		telemetry.AttributeKV{Key: "cluster-id", Value: int(cluster.ID)},
+		telemetry.AttributeKV{Key: "project-id", Value: int(cluster.ProjectID)},
+	)
+
+	event := models.PorterAppEvent{
+		ID:                 uuid.New(),
+		Status:             status,
+		Type:               eventType,
+		TypeExternalSource: externalSource,
+		PorterAppID:        app.ID,
+		Metadata:           make(map[string]any),
+	}
+
+	for k, v := range requestMetadata {
+		event.Metadata[k] = v
+	}
+
+	err = p.Repo().PorterAppEvent().CreateEvent(ctx, &event)
+	if err != nil {
+		return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error creating porter app event")
+	}
+
+	if event.ID == uuid.Nil {
+		return types.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "porter app event not found")
+	}
+
+	return event.ToPorterAppEvent(), nil
+}

+ 0 - 63
api/server/handlers/stacks/get_porter_app_events.go

@@ -1,63 +0,0 @@
-package stacks
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/google/uuid"
-	"github.com/porter-dev/porter/api/server/authz"
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
-	"github.com/porter-dev/porter/api/types"
-)
-
-type GetPorterAppEventHandler struct {
-	handlers.PorterHandlerReadWriter
-	authz.KubernetesAgentGetter
-}
-
-func NewGetPorterAppEventHandler(
-	config *config.Config,
-	writer shared.ResultWriter,
-) *GetPorterAppEventHandler {
-	return &GetPorterAppEventHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
-	}
-}
-
-func (p *GetPorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	eventID, reqErr := requestutils.GetURLParamString(r, types.URLParamStackEventID)
-	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
-		return
-	}
-
-	eventIDasUUID, err := uuid.Parse(eventID)
-	if err != nil {
-		e := fmt.Errorf("unable to parse porter app event id as uuid: %w", err)
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
-		return
-	}
-
-	if eventIDasUUID == uuid.Nil {
-		e := fmt.Errorf("invalid UUID passed for porter app event id")
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
-		return
-	}
-
-	event, err := p.Repo().PorterAppEvent().EventByID(eventIDasUUID)
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-	if event.ID == uuid.Nil {
-		e := fmt.Errorf("porter app event not found")
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusNotFound))
-		return
-	}
-
-	p.WriteResult(w, r, event.ToPorterAppEvent())
-}

+ 129 - 2
api/server/handlers/stacks/list_porter_app_events.go

@@ -1,9 +1,16 @@
 package stacks
 
 import (
+	"context"
 	"errors"
+	"fmt"
 	"net/http"
+	"reflect"
+	"strconv"
 
+	"github.com/bradleyfalzon/ghinstallation/v2"
+	"github.com/google/go-github/v41/github"
+	"github.com/google/uuid"
 	"github.com/gorilla/schema"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -13,6 +20,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository/gorm/helpers"
+	"github.com/porter-dev/porter/internal/telemetry"
 	"gorm.io/gorm"
 )
 
@@ -53,7 +61,7 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		return
 	}
 
-	porterApps, paginatedResult, err := p.Repo().PorterAppEvent().ListEventsByPorterAppID(app.ID, helpers.WithPageSize(20), helpers.WithPage(int(pr.Page)))
+	porterAppEvents, paginatedResult, err := p.Repo().PorterAppEvent().ListEventsByPorterAppID(ctx, app.ID, helpers.WithPageSize(20), helpers.WithPage(int(pr.Page)))
 	if err != nil {
 		if !errors.Is(err, gorm.ErrRecordNotFound) {
 			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
@@ -61,6 +69,17 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		}
 	}
 
+	for idx, appEvent := range porterAppEvents {
+		if appEvent.Status == "PROGRESSING" {
+			pae, err := p.updateExistingAppEvent(ctx, *cluster, stackName, appEvent.ID)
+			if err != nil {
+				p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+				return
+			}
+			porterAppEvents[idx] = &pae
+		}
+	}
+
 	res := struct {
 		Events []types.PorterAppEvent `json:"events"`
 		types.PaginationResponse
@@ -69,7 +88,7 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	}
 	res.Events = make([]types.PorterAppEvent, 0)
 
-	for _, porterApp := range porterApps {
+	for _, porterApp := range porterAppEvents {
 		if porterApp == nil {
 			continue
 		}
@@ -78,3 +97,111 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	}
 	p.WriteResult(w, r, res)
 }
+
+func (p *PorterAppEventListHandler) updateExistingAppEvent(ctx context.Context, cluster models.Cluster, stackName string, id uuid.UUID) (models.PorterAppEvent, error) {
+	ctx, span := telemetry.NewSpan(ctx, "update-porter-app-event")
+	defer span.End()
+
+	event, err := p.Repo().PorterAppEvent().ReadEvent(ctx, id)
+	if err != nil {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "error retrieving porter app by name for cluster")
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "porter-app-id", Value: event.PorterAppID},
+		telemetry.AttributeKV{Key: "porter-app-event-id", Value: event.ID.String()},
+		telemetry.AttributeKV{Key: "porter-app-event-status", Value: event.Status},
+		telemetry.AttributeKV{Key: "cluster-id", Value: int(cluster.ID)},
+		telemetry.AttributeKV{Key: "project-id", Value: int(cluster.ProjectID)},
+	)
+
+	repoOrg, ok := event.Metadata["org"].(string)
+	if !ok {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "error retrieving repo org from metadata")
+	}
+
+	repoName, ok := event.Metadata["repo"].(string)
+	if !ok {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "error retrieving repo name from metadata")
+	}
+
+	actionRunIDIface, ok := event.Metadata["action_run_id"]
+	if !ok {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "error retrieving action run id from metadata")
+	}
+	actionRunID, ok := actionRunIDIface.(float64)
+	if !ok {
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "action-run-id-type", Value: reflect.TypeOf(actionRunIDIface).String()})
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "error converting action run id to int")
+	}
+
+	accountIDIface, ok := event.Metadata["github_account_id"]
+	if !ok {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "error retrieving github account id from metadata")
+	}
+	githubAccountID, ok := accountIDIface.(float64)
+	if !ok {
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "github-account-id-type", Value: reflect.TypeOf(accountIDIface).String()})
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "error converting github account id to int")
+	}
+
+	// read the environment to get the environment id
+	env, err := p.Repo().GithubAppInstallation().ReadGithubAppInstallationByAccountID(int64(githubAccountID))
+	if err != nil {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error reading github environment by owner repo name")
+	}
+
+	ghClient, err := getGithubClientFromEnvironment(p.Config(), env.InstallationID)
+	if err != nil {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error getting github client using porter application")
+	}
+
+	actionRun, _, err := ghClient.Actions.GetWorkflowRunByID(ctx, repoOrg, repoName, int64(actionRunID))
+	if err != nil {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error getting github action run by id")
+	}
+
+	if *actionRun.Status == "completed" {
+		if *actionRun.Conclusion == "success" {
+			event.Status = "SUCCESS"
+		} else {
+			event.Status = "FAILED"
+		}
+	}
+
+	fmt.Println("STEFAN", *actionRun.Status, actionRun.Conclusion, event.Status)
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-event-updated-status", Value: event.Status})
+
+	err = p.Repo().PorterAppEvent().UpdateEvent(ctx, &event)
+	if err != nil {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error creating porter app event")
+	}
+
+	if event.ID == uuid.Nil {
+		return models.PorterAppEvent{}, telemetry.Error(ctx, span, nil, "porter app event not found")
+	}
+
+	return event, nil
+}
+
+func getGithubClientFromEnvironment(config *config.Config, installationID int64) (*github.Client, error) {
+	// get the github app client
+	ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)
+	if err != nil {
+		return nil, fmt.Errorf("malformed GITHUB_APP_ID in server configuration: %w", err)
+	}
+
+	// authenticate as github app installation
+	itr, err := ghinstallation.New(
+		http.DefaultTransport,
+		int64(ghAppId),
+		installationID,
+		config.ServerConf.GithubAppSecret,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error in creating github client from preview environment: %w", err)
+	}
+
+	return github.NewClient(&http.Client{Transport: itr}), nil
+}

+ 9 - 8
api/server/router/stack.go

@@ -226,14 +226,14 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events/{stack_event_id} -> stacks.NewPorterAppEventGetHandler
-	getPorterAppEventEndpoint := factory.NewAPIEndpoint(
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> stacks.NewCreatePorterAppEventEndpoint
+	createPorterAppEventEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}/events/{%s}", relPath, types.URLParamStackName, types.URLParamStackEventID),
+				RelativePath: fmt.Sprintf("%s/{%s}/events", relPath, types.URLParamStackName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -243,14 +243,15 @@ func getStackRoutes(
 		},
 	)
 
-	getPorterAppEventHandler := stacks.NewGetPorterAppEventHandler(
+	createPorterAppEventHandler := stacks.NewCreateUpdatePorterAppEventHandler(
 		config,
+		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &router.Route{
-		Endpoint: getPorterAppEventEndpoint,
-		Handler:  getPorterAppEventHandler,
+		Endpoint: createPorterAppEventEndpoint,
+		Handler:  createPorterAppEventHandler,
 		Router:   r,
 	})
 

+ 15 - 1
api/types/porter_app.go

@@ -74,7 +74,7 @@ type PorterAppEvent struct {
 	// UpdatedAt is the time (UTC) that an event was last updated. This can occur when an event was created as PROGRESSING, then was marked as SUCCESSFUL for example
 	UpdatedAt time.Time `json:"updated_at"`
 	// PorterAppID is the ID that the given event relates to
-	PorterAppID string         `json:"porter_app_id"`
+	PorterAppID uint           `json:"porter_app_id"`
 	Metadata    map[string]any `json:"metadata,omitempty"`
 }
 
@@ -89,3 +89,17 @@ const (
 	// PorterAppEventType_AppEvent represents a Porter Stack App Event which occurred whilst the application was running, such as an OutOfMemory (OOM) error
 	PorterAppEventType_AppEvent PorterAppEventType = "APP_EVENT"
 )
+
+// PorterAppEvent represents a simplified event for creating a Porter stack app event
+// swagger:model
+type CreateOrUpdatePorterAppEventRequest struct {
+	// ID, if supplied, will be assumed to be an update event
+	ID string `json:"id"`
+	// Status contains the accepted status' of a given event such as SUCCESS, FAILED, PROGRESSING, etc.
+	Status string `json:"status,omitempty"`
+	// Type represents a supported Porter Stack Event
+	Type PorterAppEventType `json:"type"`
+	// TypeExternalSource represents an external event source such as Github, or Gitlab. This is not always required but will commonly be see in build events
+	TypeExternalSource string         `json:"type_source,omitempty"`
+	Metadata           map[string]any `json:"metadata,omitempty"`
+}

+ 87 - 16
cli/cmd/apply.go

@@ -106,7 +106,7 @@ func init() {
 	applyCmd.MarkFlagRequired("file")
 }
 
-func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
+func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) (err error) {
 	fileBytes, err := ioutil.ReadFile(porterYAML)
 	if err != nil {
 		stackName := os.Getenv("PORTER_STACK_NAME")
@@ -177,49 +177,120 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 			Builder:              builder,
 		}
 		worker.RegisterHook("deploy-stack", deployStackHook)
+
+		if os.Getenv("GITHUB_RUN_ID") != "" {
+			// Create app event to signfy start of build
+			req := &types.CreateOrUpdatePorterAppEventRequest{
+				Status:             "PROGRESSING",
+				Type:               types.PorterAppEventType_Build,
+				TypeExternalSource: "GITHUB",
+				Metadata: map[string]any{
+					"action_run_id": os.Getenv("GITHUB_RUN_ID"),
+					"org":           os.Getenv("GITHUB_REPOSITORY_OWNER"),
+				},
+			}
+
+			repoNameSplit := strings.Split(os.Getenv("GITHUB_REPOSITORY"), "/")
+			if len(repoNameSplit) != 2 {
+				return fmt.Errorf("unable to parse GITHUB_REPOSITORY")
+			}
+			req.Metadata["repo"] = repoNameSplit[1]
+
+			actionRunID := os.Getenv("GITHUB_RUN_ID")
+			if actionRunID != "" {
+				arid, err := strconv.Atoi(actionRunID)
+				if err != nil {
+					return fmt.Errorf("unable to parse GITHUB_RUN_ID as int: %w", err)
+				}
+				req.Metadata["action_run_id"] = arid
+			}
+
+			repoOwnerAccountID := os.Getenv("GITHUB_REPOSITORY_OWNER_ID")
+			if repoOwnerAccountID != "" {
+				arid, err := strconv.Atoi(repoOwnerAccountID)
+				if err != nil {
+					return fmt.Errorf("unable to parse GITHUB_REPOSITORY_OWNER_ID as int: %w", err)
+				}
+				req.Metadata["github_account_id"] = arid
+			}
+
+			ctx := context.Background()
+			_, err := client.CreateOrUpdatePorterAppEvent(ctx, cliConf.Project, cliConf.Cluster, stackName, req)
+			if err != nil {
+				return fmt.Errorf("unable to create porter app build event: %w", err)
+			}
+		}
 	} else {
 		return fmt.Errorf("unknown porter.yaml version: %s", previewVersion.Version)
 	}
 
 	basePath, err := os.Getwd()
 	if err != nil {
-		return fmt.Errorf("error getting working directory: %w", err)
+		err = fmt.Errorf("error getting working directory: %w", err)
+		return
+	}
+
+	drivers := []struct {
+		name     string
+		funcName func(resource *switchboardModels.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error)
+	}{
+		{"deploy", NewDeployDriver},
+		{"build-image", preview.NewBuildDriver},
+		{"push-image", preview.NewPushDriver},
+		{"update-config", preview.NewUpdateConfigDriver},
+		{"random-string", preview.NewRandomStringDriver},
+		{"env-group", preview.NewEnvGroupDriver},
+		{"os-env", preview.NewOSEnvDriver},
+	}
+	for _, driver := range drivers {
+		err = worker.RegisterDriver(driver.name, driver.funcName)
+		if err != nil {
+			err = fmt.Errorf("error registering driver %s: %w", driver.name, err)
+			return
+		}
 	}
 
-	worker.RegisterDriver("deploy", NewDeployDriver)
-	worker.RegisterDriver("build-image", preview.NewBuildDriver)
-	worker.RegisterDriver("push-image", preview.NewPushDriver)
-	worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
-	worker.RegisterDriver("random-string", preview.NewRandomStringDriver)
-	worker.RegisterDriver("env-group", preview.NewEnvGroupDriver)
-	worker.RegisterDriver("os-env", preview.NewOSEnvDriver)
-
 	worker.SetDefaultDriver("deploy")
 
 	if hasDeploymentHookEnvVars() {
 		deplNamespace := os.Getenv("PORTER_NAMESPACE")
 
 		if deplNamespace == "" {
-			return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
+			err = fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
+			return
 		}
 
 		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
 		if err != nil {
-			return fmt.Errorf("error creating deployment hook: %w", err)
+			err = fmt.Errorf("error creating deployment hook: %w", err)
+			return err
 		}
 
-		worker.RegisterHook("deployment", deploymentHook)
+		err = worker.RegisterHook("deployment", deploymentHook)
+		if err != nil {
+			err = fmt.Errorf("error registering deployment hook: %w", err)
+			return err
+		}
 	}
 
 	errorEmitterHook := NewErrorEmitterHook(client, resGroup)
-	worker.RegisterHook("erroremitter", errorEmitterHook)
+	err = worker.RegisterHook("erroremitter", errorEmitterHook)
+	if err != nil {
+		err = fmt.Errorf("error registering error emitter hook: %w", err)
+		return err
+	}
 
 	cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
-	worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
+	err = worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
+	if err != nil {
+		err = fmt.Errorf("error registering clone env group hook: %w", err)
+		return err
+	}
 
-	return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
+	err = worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
 		BasePath: basePath,
 	})
+	return
 }
 
 func applyValidate() error {

+ 6 - 1
ee/api/server/handlers/invite/accept.go

@@ -15,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
 	"gorm.io/gorm"
 )
 
@@ -31,7 +32,11 @@ func NewInviteAcceptHandler(
 }
 
 func (c *InviteAcceptHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-invite-accept")
+	defer span.End()
+
+	user, _ := ctx.Value(types.UserScope).(*models.User)
+
 	projectID, _ := requestutils.GetURLParamUint(r, types.URLParamProjectID)
 	token, _ := requestutils.GetURLParamString(r, types.URLParamInviteToken)
 

+ 5 - 2
ee/api/server/handlers/invite/create.go

@@ -35,8 +35,11 @@ func NewInviteCreateHandler(
 }
 
 func (c *InviteCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	user, _ := r.Context().Value(types.UserScope).(*models.User)
-	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-invite-create")
+	defer span.End()
+
+	user, _ := ctx.Value(types.UserScope).(*models.User)
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 	request := &types.CreateInviteRequest{}
 

+ 6 - 3
internal/models/porter_app_event.go

@@ -25,8 +25,8 @@ type PorterAppEvent struct {
 	// UpdatedAt is the time (UTC) that an event was last updated. This can occur when an event was created as PROGRESSING, then was marked as SUCCESSFUL for example
 	UpdatedAt time.Time `json:"updated_at"`
 	// PorterAppID is the ID that the given event relates to
-	PorterAppID string `json:"porter_app_id"`
-	Metadata    JSONB  `json:"metadata" sql:"type:jsonb" gorm:"type:jsonb"`
+	PorterAppID uint  `json:"porter_app_id"`
+	Metadata    JSONB `json:"metadata" sql:"type:jsonb" gorm:"type:jsonb"`
 }
 
 // TableName overrides the table name
@@ -46,7 +46,10 @@ func (p *PorterAppEvent) ToPorterAppEvent() types.PorterAppEvent {
 		CreatedAt:          p.CreatedAt,
 		UpdatedAt:          p.UpdatedAt,
 		PorterAppID:        p.PorterAppID,
-		Metadata:           p.Metadata,
 	}
+	if p.Metadata != nil {
+		ty.Metadata = p.Metadata
+	}
+
 	return ty
 }

+ 53 - 11
internal/repository/gorm/porter_app_event.go

@@ -1,9 +1,10 @@
 package gorm
 
 import (
+	"context"
 	"errors"
-	"fmt"
 	"strconv"
+	"time"
 
 	"github.com/google/uuid"
 	"github.com/porter-dev/porter/internal/models"
@@ -23,7 +24,7 @@ func NewPorterAppEventRepository(db *gorm.DB) repository.PorterAppEventRepositor
 	return &PorterAppEventRepository{db}
 }
 
-func (repo *PorterAppEventRepository) ListEventsByPorterAppID(porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error) {
+func (repo *PorterAppEventRepository) ListEventsByPorterAppID(ctx context.Context, porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error) {
 	apps := []*models.PorterAppEvent{}
 	paginatedResult := helpers.PaginatedResult{}
 
@@ -35,7 +36,7 @@ func (repo *PorterAppEventRepository) ListEventsByPorterAppID(porterAppID uint,
 	db := repo.db.Model(&models.PorterAppEvent{})
 	db = db.Scopes(helpers.Paginate(db, &paginatedResult, opts...))
 
-	if err := db.Where("porter_app_id = ?", id).Find(&apps).Error; err != nil {
+	if err := db.Where("porter_app_id = ?", id).Order("updated_at DESC").Find(&apps).Error; err != nil {
 		if !errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, paginatedResult, err
 		}
@@ -44,17 +45,58 @@ func (repo *PorterAppEventRepository) ListEventsByPorterAppID(porterAppID uint,
 	return apps, paginatedResult, nil
 }
 
-func (repo *PorterAppEventRepository) EventByID(eventID uuid.UUID) (*models.PorterAppEvent, error) {
-	app := &models.PorterAppEvent{}
+func (repo *PorterAppEventRepository) CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error {
+	if appEvent.ID == uuid.Nil {
+		appEvent.ID = uuid.New()
+	}
+	if appEvent.CreatedAt.IsZero() {
+		appEvent.CreatedAt = time.Now().UTC()
+	}
+	if appEvent.UpdatedAt.IsZero() {
+		appEvent.UpdatedAt = time.Now().UTC()
+	}
+	if appEvent.PorterAppID == 0 {
+		return errors.New("invalid porter app id supplied")
+	}
 
-	if eventID == uuid.Nil {
-		return app, errors.New("invalid porter app event id supplied")
+	if err := repo.db.Create(appEvent).Error; err != nil {
+		return err
+	}
+	return nil
+}
+
+func (repo *PorterAppEventRepository) UpdateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error {
+	if appEvent.ID == uuid.Nil {
+		appEvent.ID = uuid.New()
+	}
+	if appEvent.UpdatedAt.IsZero() {
+		appEvent.UpdatedAt = time.Now().UTC()
+	}
+	if appEvent.PorterAppID == 0 {
+		return errors.New("invalid porter app id supplied")
 	}
+	if appEvent.Status == "" {
+		return errors.New("invalid status supplied")
+	}
+
+	if err := repo.db.Model(appEvent).Updates(models.PorterAppEvent{Status: appEvent.Status}).Error; err != nil {
+		return err
+	}
+	return nil
+}
+
+func (repo *PorterAppEventRepository) ReadEvent(ctx context.Context, id uuid.UUID) (models.PorterAppEvent, error) {
+	appEvent := models.PorterAppEvent{}
+
+	if id == uuid.Nil {
+		return appEvent, errors.New("invalid porter app event id supplied")
+	}
+
+	strID := id.String()
 
-	tx := repo.db.Find(&app, "id = ?", eventID.String())
-	if tx.Error != nil {
-		return app, fmt.Errorf("no porter app event found for id %s: %w", eventID, tx.Error)
+	if err := repo.db.Where("id = ?", strID).First(&appEvent).Error; err != nil {
+		return appEvent, err
 	}
 
-	return app, nil
+	return appEvent, nil
 }

+ 6 - 2
internal/repository/porter_app_event.go

@@ -1,6 +1,8 @@
 package repository
 
 import (
+	"context"
+
 	"github.com/google/uuid"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository/gorm/helpers"
@@ -8,6 +10,8 @@ import (
 
 // PorterAppEventRepository represents the set of queries on the PorterAppEvent model
 type PorterAppEventRepository interface {
-	ListEventsByPorterAppID(porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error)
-	EventByID(eventID uuid.UUID) (*models.PorterAppEvent, error)
+	ListEventsByPorterAppID(ctx context.Context, porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error)
+	CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error
+	UpdateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error
+	ReadEvent(ctx context.Context, id uuid.UUID) (models.PorterAppEvent, error)
 }

+ 12 - 3
internal/repository/test/porter_app_event.go

@@ -1,6 +1,7 @@
 package test
 
 import (
+	"context"
 	"errors"
 
 	"github.com/google/uuid"
@@ -17,10 +18,18 @@ func NewPorterAppEventRepository(canQuery bool, failingMethods ...string) reposi
 	return &PorterAppEventRepository{canQuery: false}
 }
 
-func (repo *PorterAppEventRepository) ListEventsByPorterAppID(porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error) {
+func (repo *PorterAppEventRepository) ListEventsByPorterAppID(ctx context.Context, porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error) {
 	return nil, helpers.PaginatedResult{}, errors.New("cannot write database")
 }
 
-func (repo *PorterAppEventRepository) EventByID(eventID uuid.UUID) (*models.PorterAppEvent, error) {
-	return &models.PorterAppEvent{}, errors.New("cannot write database")
+func (repo *PorterAppEventRepository) CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error {
+	return errors.New("cannot write database")
+}
+
+func (repo *PorterAppEventRepository) UpdateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error {
+	return errors.New("cannot update database")
+}
+
+func (repo *PorterAppEventRepository) ReadEvent(ctx context.Context, id uuid.UUID) (models.PorterAppEvent, error) {
+	return models.PorterAppEvent{}, errors.New("cannot read database")
 }

+ 10 - 1
zarf/helm/.serverenv

@@ -1,6 +1,7 @@
 # Fill out this file, and renamed to '.server.env' in order to run this with Tilt
 
 # Required parameters
+
 SQL_LITE=false
 DB_NAME=porter
 DB_USER=porter
@@ -9,14 +10,17 @@ DB_HOST=postgresql
 DB_PORT=5432
 
 # Required for accessing cluster control plane. If ENABLE_CAPI_PROVISIONER=false, nothing in this section will be used
+
 ENABLE_CAPI_PROVISIONER=false
 NATS_URL=nats:4222
 CLUSTER_CONTROL_PLANE_ADDRESS=http://ccp-web:7833
 
 # Github Login OAuth
+
 GITHUB_LOGIN_ENABLED=false
 
 # Github App for repo deployments, and preview environments. Remove these if you are not using preview environments or deploying from a repo locally
+
 GITHUB_APP_CLIENT_ID=<github_app_id>
 GITHUB_APP_CLIENT_SECRET=<github_secret>
 GITHUB_APP_WEBHOOK_SECRET=<webhook_secret>
@@ -24,14 +28,19 @@ GITHUB_APP_NAME=<github_app_name>
 GITHUB_APP_ID=<github_app_id>
 
 # GITHUB_APP_SECRET_PATH is the path to your secret within the container. Tilt will sync your ~/.ssh/ folder into /app/ssh automatically. This will likely be /app/ssh/your_ssh_pem_name
+
 GITHUB_APP_SECRET_PATH=<path_to_secret>
 
 # Optional parameters
+
 HELM_APP_REPO_URL=https://charts.getporter.dev
 HELM_ADD_ON_REPO_URL=https://charts.getporter.dev
 
 # SERVER_URL must be set to your ngrok url, If you are using ngrok for your github app setup on the backend
+
 # SERVER_URL=
 
+# TELEMETRY\_\* are required if you are piping trace events to the open telemetry collector
+
 TELEMETRY_NAME=porter
-TELEMETRY_COLLECTOR_URL=otel-collector:4317
+TELEMETRY_COLLECTOR_URL=otel-collector:4317