Procházet zdrojové kódy

support app-events on new flow (#3533)

Co-authored-by: David Townley <davidtownley@Davids-MacBook-Air.local>
Co-authored-by: Feroze Mohideen <feroze@porter.run>
d-g-town před 2 roky
rodič
revize
a68db77633

+ 110 - 44
api/server/handlers/porter_app/create_and_update_events.go

@@ -61,6 +61,7 @@ func (p *CreateUpdatePorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *
 		telemetry.AttributeKV{Key: "porter-app-event-status", Value: request.Status},
 		telemetry.AttributeKV{Key: "porter-app-event-external-source", Value: request.TypeExternalSource},
 		telemetry.AttributeKV{Key: "porter-app-event-id", Value: request.ID},
+		telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID},
 	)
 
 	if request.Type == types.PorterAppEventType_Build {
@@ -68,7 +69,7 @@ func (p *CreateUpdatePorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *
 	}
 
 	if request.ID == "" {
-		event, err := p.createNewAppEvent(ctx, *cluster, appName, request.Status, string(request.Type), request.TypeExternalSource, request.Metadata)
+		event, err := p.createNewAppEvent(ctx, *cluster, appName, request.DeploymentTargetID, request.Status, string(request.Type), request.TypeExternalSource, request.Metadata)
 		if err != nil {
 			e := telemetry.Error(ctx, span, err, "error creating new app event")
 			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
@@ -116,7 +117,7 @@ func reportBuildStatus(ctx context.Context, request *types.CreateOrUpdatePorterA
 }
 
 // createNewAppEvent will create a new app event for the given porter app name. If the app event is an agent event, then it will be created only if there is no existing event which has the agent ID. In the case that an existing event is found, that will be returned instead
-func (p *CreateUpdatePorterAppEventHandler) createNewAppEvent(ctx context.Context, cluster models.Cluster, porterAppName string, status types.PorterAppEventStatus, eventType string, externalSource string, requestMetadata map[string]any) (types.PorterAppEvent, error) {
+func (p *CreateUpdatePorterAppEventHandler) createNewAppEvent(ctx context.Context, cluster models.Cluster, porterAppName string, deploymentTargetID string, status types.PorterAppEventStatus, eventType string, externalSource string, requestMetadata map[string]any) (types.PorterAppEvent, error) {
 	ctx, span := telemetry.NewSpan(ctx, "create-porter-app-event")
 	defer span.End()
 
@@ -138,13 +139,29 @@ func (p *CreateUpdatePorterAppEventHandler) createNewAppEvent(ctx context.Contex
 		// Agent has no way to know what the porter app event id is, so if we must dedup here
 		// TODO: create a filter to filter by only agent events. Not an issue now as app events are deduped per hour on the agent side
 		if agentEventID, ok := requestMetadata["agent_event_id"]; ok {
-			existingEvents, _, err := p.Repo().PorterAppEvent().ListEventsByPorterAppID(ctx, app.ID)
-			if err != nil {
-				return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error listing porter app events for event type")
+			var existingEvents []*models.PorterAppEvent
+			if deploymentTargetID == "" {
+				existingEvents, _, err = p.Repo().PorterAppEvent().ListEventsByPorterAppID(ctx, app.ID)
+				if err != nil {
+					return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error listing porter app events for event type")
+				}
+			} else {
+				deploymentTargetUUID, err := uuid.Parse(deploymentTargetID)
+				if err != nil {
+					return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error parsing deployment target id")
+				}
+				if deploymentTargetUUID == uuid.Nil {
+					return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "deployment target id cannot be nil")
+				}
+
+				existingEvents, _, err = p.Repo().PorterAppEvent().ListEventsByPorterAppIDAndDeploymentTargetID(ctx, app.ID, deploymentTargetUUID)
+				if err != nil {
+					return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error listing porter app events for event type with deployment target id")
+				}
 			}
 
 			for _, existingEvent := range existingEvents {
-				if existingEvent.Type == eventType {
+				if existingEvent != nil && existingEvent.Type == eventType {
 					existingAgentEventID, ok := existingEvent.Metadata["agent_event_id"]
 					if !ok {
 						continue
@@ -163,7 +180,7 @@ func (p *CreateUpdatePorterAppEventHandler) createNewAppEvent(ctx context.Contex
 	if eventType == string(types.PorterAppEventType_Deploy) {
 		// Agent has no way to know what the porter app event id is, so update the deploy event if it exists
 		if _, ok := requestMetadata["deploy_status"]; ok {
-			return p.updateDeployEvent(ctx, porterAppName, app.ID, requestMetadata), nil
+			return p.updateDeployEvent(ctx, porterAppName, app.ID, deploymentTargetID, requestMetadata), nil
 		}
 	}
 
@@ -176,6 +193,17 @@ func (p *CreateUpdatePorterAppEventHandler) createNewAppEvent(ctx context.Contex
 		Metadata:           make(map[string]any),
 	}
 
+	if deploymentTargetID != "" {
+		deploymentTargetUUID, err := uuid.Parse(deploymentTargetID)
+		if err != nil {
+			return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "error parsing deployment target id")
+		}
+		if deploymentTargetUUID == uuid.Nil {
+			return types.PorterAppEvent{}, telemetry.Error(ctx, span, err, "deployment target id cannot be nil")
+		}
+		event.DeploymentTargetID = deploymentTargetUUID
+	}
+
 	for k, v := range requestMetadata {
 		event.Metadata[k] = v
 	}
@@ -246,40 +274,86 @@ func (p *CreateUpdatePorterAppEventHandler) updateExistingAppEvent(ctx context.C
 // 4. the services specified in the updatedStatusMetadata match the services in the deploy event metadata
 // 5. some of the above services are still in the PROGRESSING state
 // if one of these conditions is not met, then an empty event is returned and no update is made; otherwise, the matched event is returned
-func (p *CreateUpdatePorterAppEventHandler) updateDeployEvent(ctx context.Context, appName string, appID uint, updatedStatusMetadata map[string]any) types.PorterAppEvent {
+func (p *CreateUpdatePorterAppEventHandler) updateDeployEvent(ctx context.Context, appName string, appID uint, deploymentTargetID string, updatedStatusMetadata map[string]any) types.PorterAppEvent {
 	ctx, span := telemetry.NewSpan(ctx, "update-deploy-event")
 	defer span.End()
 
-	revision, ok := updatedStatusMetadata["revision"]
-	if !ok {
-		_ = telemetry.Error(ctx, span, nil, "revision not found in request metadata")
-		return types.PorterAppEvent{}
-	}
-	revisionFloat64, ok := revision.(float64)
-	if !ok {
-		_ = telemetry.Error(ctx, span, nil, "revision not a float64")
-		return types.PorterAppEvent{}
-	}
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "revision", Value: revisionFloat64})
+	var serviceName string
+	var matchEvent models.PorterAppEvent
 
-	podName, ok := updatedStatusMetadata["pod_name"]
-	if !ok {
-		_ = telemetry.Error(ctx, span, nil, "pod name not found in request metadata")
-		return types.PorterAppEvent{}
-	}
-	podNameStr, ok := podName.(string)
-	if !ok {
-		_ = telemetry.Error(ctx, span, nil, "pod name not a string")
-		return types.PorterAppEvent{}
-	}
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "pod-name", Value: podNameStr})
+	if deploymentTargetID != "" {
+		appRevisionIDField, ok := updatedStatusMetadata["app_revision_id"]
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "app_revision_id not found in request metadata")
+			return types.PorterAppEvent{}
+		}
+		appRevisionID, ok := appRevisionIDField.(string)
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "appRevisionID is not a string")
+			return types.PorterAppEvent{}
+		}
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-revision-id", Value: appRevisionID})
 
-	serviceName := getServiceNameFromPodName(podNameStr, appName)
-	if serviceName == "" {
-		_ = telemetry.Error(ctx, span, nil, "service name not found in pod name")
-		return types.PorterAppEvent{}
+		serviceNameField, ok := updatedStatusMetadata["service_name"]
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "service_name not found in request metadata")
+			return types.PorterAppEvent{}
+		}
+		serviceName, ok = serviceNameField.(string)
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "serviceName is not a string")
+			return types.PorterAppEvent{}
+		}
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "service-name", Value: serviceName})
+
+		var err error
+		matchEvent, err = p.Repo().PorterAppEvent().ReadDeployEventByAppRevisionID(ctx, appID, appRevisionID)
+		if err != nil {
+			_ = telemetry.Error(ctx, span, err, "error finding matching deploy event")
+			return types.PorterAppEvent{}
+		}
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-deployment-event", Value: false})
+	} else {
+		revision, ok := updatedStatusMetadata["revision"]
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "revision not found in request metadata")
+			return types.PorterAppEvent{}
+		}
+		revisionFloat64, ok := revision.(float64)
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "revision not a float64")
+			return types.PorterAppEvent{}
+		}
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "revision", Value: revisionFloat64})
+
+		podName, ok := updatedStatusMetadata["pod_name"]
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "pod name not found in request metadata")
+			return types.PorterAppEvent{}
+		}
+		podNameStr, ok := podName.(string)
+		if !ok {
+			_ = telemetry.Error(ctx, span, nil, "pod name not a string")
+			return types.PorterAppEvent{}
+		}
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "pod-name", Value: podNameStr})
+
+		serviceName = getServiceNameFromPodName(podNameStr, appName)
+		if serviceName == "" {
+			_ = telemetry.Error(ctx, span, nil, "service name not found in pod name")
+			return types.PorterAppEvent{}
+		}
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "service-name", Value: serviceName})
+
+		var err error
+		matchEvent, err = p.Repo().PorterAppEvent().ReadDeployEventByRevision(ctx, appID, revisionFloat64)
+		if err != nil {
+			_ = telemetry.Error(ctx, span, err, "error finding matching deploy event")
+			return types.PorterAppEvent{}
+		}
+
+		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-deployment-event", Value: false})
 	}
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "service-name", Value: serviceName})
 
 	newStatus, ok := updatedStatusMetadata["deploy_status"]
 	if !ok {
@@ -306,14 +380,6 @@ func (p *CreateUpdatePorterAppEventHandler) updateDeployEvent(ctx context.Contex
 
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "new-status", Value: string(porterAppEventStatus)})
 
-	matchEvent, err := p.Repo().PorterAppEvent().ReadDeployEventByRevision(ctx, appID, revisionFloat64)
-	if err != nil {
-		_ = telemetry.Error(ctx, span, err, "error finding matching deploy event")
-		return types.PorterAppEvent{}
-	}
-
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-deployment-event", Value: false})
-
 	// first check to see if the event is empty, meaning there was no match found, or not progressing, meaning it has already been updated
 	if matchEvent.ID == uuid.Nil || matchEvent.Status != string(types.PorterAppEventStatus_Progressing) {
 		return types.PorterAppEvent{}
@@ -382,7 +448,7 @@ func (p *CreateUpdatePorterAppEventHandler) updateDeployEvent(ctx context.Contex
 			}
 		}
 
-		err = p.Repo().PorterAppEvent().UpdateEvent(ctx, &matchEvent)
+		err := p.Repo().PorterAppEvent().UpdateEvent(ctx, &matchEvent)
 		if err != nil {
 			_ = telemetry.Error(ctx, span, err, "error updating deploy event")
 			return matchEvent.ToPorterAppEvent()

+ 5 - 2
api/types/porter_app.go

@@ -93,8 +93,10 @@ 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 uint           `json:"porter_app_id"`
-	Metadata    map[string]any `json:"metadata,omitempty"`
+	PorterAppID uint `json:"porter_app_id"`
+	// DeploymentTargetID is the ID of the deployment target that the given event relates to
+	DeploymentTargetID string         `json:"deployment_target_id"`
+	Metadata           map[string]any `json:"metadata,omitempty"`
 }
 
 // PorterAppEventType is an alias for a string that represents a Porter Stack Event Type
@@ -137,6 +139,7 @@ type CreateOrUpdatePorterAppEventRequest struct {
 	// 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"`
+	DeploymentTargetID string         `json:"deployment_target_id"`
 }
 
 // ServiceDeploymentMetadata contains information about a service when it deploys

+ 5 - 2
internal/models/porter_app_event.go

@@ -25,8 +25,10 @@ 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 uint  `json:"porter_app_id"`
-	Metadata    JSONB `json:"metadata" sql:"type:jsonb" gorm:"type:jsonb"`
+	PorterAppID uint `json:"porter_app_id" gorm:"index:idx_app_deployment_target"`
+	// DeploymentTargetID is the ID of the deployment target that the event relates to
+	DeploymentTargetID uuid.UUID `json:"deployment_target_id" gorm:"type:uuid;index:idx_app_deployment_target;default:00000000-0000-0000-0000-000000000000"`
+	Metadata           JSONB     `json:"metadata" sql:"type:jsonb" gorm:"type:jsonb"`
 }
 
 // TableName overrides the table name
@@ -46,6 +48,7 @@ func (p *PorterAppEvent) ToPorterAppEvent() types.PorterAppEvent {
 		CreatedAt:          p.CreatedAt,
 		UpdatedAt:          p.UpdatedAt,
 		PorterAppID:        p.PorterAppID,
+		DeploymentTargetID: p.DeploymentTargetID.String(),
 	}
 	if p.Metadata != nil {
 		ty.Metadata = p.Metadata

+ 49 - 0
internal/repository/gorm/porter_app_event.go

@@ -47,6 +47,33 @@ func (repo *PorterAppEventRepository) ListEventsByPorterAppID(ctx context.Contex
 	return apps, paginatedResult, nil
 }
 
+// ListEventsByPorterAppIDAndDeploymentTargetID returns a list of events for a given porter app id and deployment target id
+func (repo *PorterAppEventRepository) ListEventsByPorterAppIDAndDeploymentTargetID(ctx context.Context, porterAppID uint, deploymentTargetID uuid.UUID, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error) {
+	apps := []*models.PorterAppEvent{}
+	paginatedResult := helpers.PaginatedResult{}
+
+	id := strconv.Itoa(int(porterAppID))
+	if id == "" {
+		return nil, paginatedResult, errors.New("invalid porter app id supplied")
+	}
+
+	if deploymentTargetID == uuid.Nil {
+		return nil, paginatedResult, errors.New("invalid deployment target id supplied")
+	}
+
+	db := repo.db.Model(&models.PorterAppEvent{})
+	resultDB := db.Where("porter_app_id = ? AND deployment_target_id = ?", id, deploymentTargetID).Order("created_at DESC")
+	resultDB = resultDB.Scopes(helpers.Paginate(db, &paginatedResult, opts...))
+
+	if err := resultDB.Find(&apps).Error; err != nil {
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, paginatedResult, err
+		}
+	}
+
+	return apps, paginatedResult, nil
+}
+
 func (repo *PorterAppEventRepository) CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error {
 	if appEvent.ID == uuid.Nil {
 		appEvent.ID = uuid.New()
@@ -126,3 +153,25 @@ func (repo *PorterAppEventRepository) ReadDeployEventByRevision(ctx context.Cont
 
 	return appEvent, nil
 }
+
+// ReadDeployEventByAppRevisionID returns a deploy event for a given porter app id and app revision ID
+func (repo *PorterAppEventRepository) ReadDeployEventByAppRevisionID(ctx context.Context, porterAppID uint, appRevisionID string) (models.PorterAppEvent, error) {
+	appEvent := models.PorterAppEvent{}
+
+	if porterAppID == 0 {
+		return appEvent, errors.New("invalid porter app ID supplied")
+	}
+
+	if appRevisionID == "" {
+		return appEvent, errors.New("no app revision ID supplied")
+	}
+
+	// Convert porterAppID to string
+	strAppID := strconv.Itoa(int(porterAppID))
+
+	if err := repo.db.Where("porter_app_id = ? AND type = 'DEPLOY' AND metadata->>'app_revision_id' = ?", strAppID, appRevisionID).First(&appEvent).Error; err != nil {
+		return appEvent, err
+	}
+
+	return appEvent, nil
+}

+ 4 - 0
internal/repository/porter_app_event.go

@@ -11,8 +11,12 @@ import (
 // PorterAppEventRepository represents the set of queries on the PorterAppEvent model
 type PorterAppEventRepository interface {
 	ListEventsByPorterAppID(ctx context.Context, porterAppID uint, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error)
+	// ListEventsByPorterAppIDAndDeploymentTargetID returns a list of events for a given porter app id and deployment target id
+	ListEventsByPorterAppIDAndDeploymentTargetID(ctx context.Context, porterAppID uint, deploymentTargetID uuid.UUID, 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)
 	ReadDeployEventByRevision(ctx context.Context, porterAppID uint, revision float64) (models.PorterAppEvent, error)
+	// ReadDeployEventByAppRevisionID returns a deploy event for a given porter app id and app revision ID
+	ReadDeployEventByAppRevisionID(ctx context.Context, porterAppID uint, appRevisionID string) (models.PorterAppEvent, error)
 }

+ 10 - 0
internal/repository/test/porter_app_event.go

@@ -22,6 +22,11 @@ func (repo *PorterAppEventRepository) ListEventsByPorterAppID(ctx context.Contex
 	return nil, helpers.PaginatedResult{}, errors.New("cannot write database")
 }
 
+// ListEventsByPorterAppIDAndDeploymentTargetID is a test method
+func (repo *PorterAppEventRepository) ListEventsByPorterAppIDAndDeploymentTargetID(ctx context.Context, porterAppID uint, deploymentTargetID uuid.UUID, opts ...helpers.QueryOption) ([]*models.PorterAppEvent, helpers.PaginatedResult, error) {
+	return nil, helpers.PaginatedResult{}, errors.New("cannot write database")
+}
+
 func (repo *PorterAppEventRepository) CreateEvent(ctx context.Context, appEvent *models.PorterAppEvent) error {
 	return errors.New("cannot write database")
 }
@@ -37,3 +42,8 @@ func (repo *PorterAppEventRepository) ReadEvent(ctx context.Context, id uuid.UUI
 func (repo *PorterAppEventRepository) ReadDeployEventByRevision(ctx context.Context, porterAppID uint, revision float64) (models.PorterAppEvent, error) {
 	return models.PorterAppEvent{}, errors.New("cannot read database")
 }
+
+// ReadDeployEventByAppRevisionID is a test method
+func (repo *PorterAppEventRepository) ReadDeployEventByAppRevisionID(ctx context.Context, porterAppID uint, appRevisionID string) (models.PorterAppEvent, error) {
+	return models.PorterAppEvent{}, errors.New("cannot read database")
+}