dgtown 2 anos atrás
pai
commit
53248bf52d

+ 45 - 14
api/server/handlers/porter_app/list_events_apply_v2.go

@@ -1,9 +1,12 @@
 package porter_app
 
 import (
-	"errors"
+	"encoding/json"
+	"fmt"
 	"net/http"
 
+	"github.com/porter-dev/porter/internal/repository/gorm/helpers"
+
 	"github.com/google/uuid"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -12,9 +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/repository/gorm/helpers"
 	"github.com/porter-dev/porter/internal/telemetry"
-	"gorm.io/gorm"
 )
 
 // PorterAppV2EventListHandler handles the /apps/{app_name}/events endpoint (used for validate_apply v2)
@@ -72,20 +73,18 @@ func (p *PorterAppV2EventListHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		return
 	}
 
-	app, err := p.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
+	appInstance, err := p.Repo().AppInstance().FromNameAndDeploymentTargetId(ctx, appName, uid.String())
 	if err != nil {
 		err = telemetry.Error(ctx, span, err, "error retrieving porter app by name")
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
-	porterAppEvents, paginatedResult, err := p.Repo().PorterAppEvent().ListBuildDeployEventsByPorterAppIDAndDeploymentTargetID(ctx, app.ID, uid, helpers.WithPageSize(20), helpers.WithPage(int(request.Page)))
+	revisions, paginatedResult, err := p.Repo().AppRevision().Revisions(cluster.ProjectID, appInstance.ID.String(), helpers.WithPageSize(20), helpers.WithPage(int(request.Page)))
 	if err != nil {
-		if !errors.Is(err, gorm.ErrRecordNotFound) {
-			e := telemetry.Error(ctx, span, nil, "error listing porter app events by porter app id")
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
-			return
-		}
+		err = telemetry.Error(ctx, span, err, "error retrieving app revisions")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
 	}
 
 	res := struct {
@@ -96,12 +95,44 @@ func (p *PorterAppV2EventListHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 	res.Events = make([]types.PorterAppEvent, 0)
 
-	for _, porterApp := range porterAppEvents {
-		if porterApp == nil {
+	for _, revision := range revisions {
+		if revision == nil {
 			continue
 		}
-		pa := porterApp.ToPorterAppEvent()
-		res.Events = append(res.Events, pa)
+		by, err := revision.Metadata.Value()
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error getting metadata value")
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		strMetadata, ok := by.(string)
+		if !ok {
+			err = telemetry.Error(ctx, span, nil, "error getting string metadata value")
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		fmt.Println(strMetadata)
+
+		var events struct {
+			Events []types.PorterAppEvent
+		}
+
+		err = json.Unmarshal([]byte(strMetadata), &events)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error unmarshalling metadata value")
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		for i := range events.Events {
+			events.Events[i].DeploymentTargetID = request.DeploymentTargetId
+			events.Events[i].PorterAppID = uint(revision.PorterAppID)
+			events.Events[i].AppRevisionID = revision.ID.String()
+		}
+
+		res.Events = append(res.Events, events.Events...)
 	}
 	p.WriteResult(w, r, res)
 }

+ 148 - 0
api/server/handlers/porter_app/revisions.go

@@ -0,0 +1,148 @@
+package porter_app
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/internal/repository/gorm/helpers"
+
+	"connectrpc.com/connect"
+	"github.com/google/uuid"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"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/porter_app"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// RevisionsHandler handles requests to the targets/{deployment_target_identifier}/apps/{porter_app_name}/revisions endpoint
+type RevisionsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewRevisionsHandler returns a new RevisionsHandler
+func NewRevisionsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *RevisionsHandler {
+	return &RevisionsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// RevisionsRequest represents the response from the /apps/{porter_app_name}/revisions endpoint
+type RevisionsRequest struct {
+	types.PaginationRequest
+}
+
+// RevisionsResponse represents the response from the /apps/{porter_app_name}/revisions endpoint
+type RevisionsResponse struct {
+	AppRevisionsWithEvents []AppRevisionWithEvents `json:"app_revisions_with_events"`
+	types.PaginationResponse
+}
+
+type AppRevisionWithEvents struct {
+	Revision porter_app.Revision `json:"revision"`
+	Events   []Event             `json:"events"`
+}
+
+type Event struct {
+	Type      string `json:"type"`
+	Status    string `json:"status"`
+	StartedAt string `json:"started_at"`
+	EndedAt   string `json:"ended_at"`
+}
+
+func (c *RevisionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-revisions")
+	defer span.End()
+
+	deploymentTarget, ok := ctx.Value(types.DeploymentTargetScope).(types.DeploymentTarget)
+	if !ok {
+		err := telemetry.Error(ctx, span, nil, "deployment target not found")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing porter app name")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
+
+	request := &RevisionsRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "invalid request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appInstance, err := c.Repo().AppInstance().FromNameAndDeploymentTargetId(ctx, appName, deploymentTarget.ID.String())
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error retrieving porter app by name")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	revisions, paginatedResult, err := c.Repo().AppRevision().Revisions(deploymentTarget.ProjectID, appInstance.ID.String(), helpers.WithPageSize(20), helpers.WithPage(int(request.Page)))
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error retrieving app revisions")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	var resp RevisionsResponse
+
+	for _, revision := range revisions {
+		var appRevisionWith
+		if revision == nil {
+			continue
+		}
+		by, err := revision.Metadata.Value()
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error getting metadata value")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		strMetadata, ok := by.(string)
+		if !ok {
+			err = telemetry.Error(ctx, span, nil, "error getting string metadata value")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		var events struct {
+			Events []types.PorterAppEvent
+		}
+
+		err = json.Unmarshal([]byte(strMetadata), &events)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error unmarshalling metadata value")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		for i := range events.Events {
+			events.Events[i].DeploymentTargetID = request.DeploymentTargetId
+			events.Events[i].PorterAppID = uint(revision.PorterAppID)
+			events.Events[i].AppRevisionID = revision.ID.String()
+		}
+
+		res.Events = append(res.Events, events.Events...)
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 4 - 2
api/types/porter_app.go

@@ -95,8 +95,10 @@ type PorterAppEvent struct {
 	// PorterAppID is the ID that the given event relates to
 	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"`
+	DeploymentTargetID string `json:"deployment_target_id"`
+	// AppRevisionId is the ID of the app revision that the given event relates to
+	AppRevisionID string         `json:"app_revision_id"`
+	Metadata      map[string]any `json:"metadata,omitempty"`
 }
 
 // PorterAppAppEventMetadata represents the metadata for a Porter App Event of type APP_EVENT

+ 1 - 0
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/ActivityFeed.tsx

@@ -159,6 +159,7 @@ const ActivityFeed: React.FC<Props> = ({
   }
 
   if (isEventFetchLoading || porterAgentCheckLoading || events == null) {
+    console.log(isEventFetchLoading, porterAgentCheckLoading, events);
     return (
       <div>
         <Spacer y={2} />

+ 14 - 14
dashboard/src/main/home/app-dashboard/app-view/tabs/activity-feed/events/types.ts

@@ -18,19 +18,7 @@ const porterAppAppEventMetadataValidator = z.object({
   agent_event_id: z.number(),
 });
 const porterAppDeployEventMetadataValidator = z.object({
-  image_tag: z.string().optional(),
-  app_revision_id: z.string(),
-  rollback_target_app_revision_id: z.string().optional(),
-  rollback_target_image_tag: z.string().optional(),
-  service_deployment_metadata: z
-    .record(
-      z.object({
-        status: z.string(),
-        type: z.string(),
-      })
-    )
-    .optional(),
-  end_time: z.string().optional(),
+
 });
 const porterAppBuildEventMetadataValidator = z.object({
   repo: z.string().optional(),
@@ -161,7 +149,19 @@ export const porterAppEventValidator = z.discriminatedUnion("type", [
     type: z.literal("DEPLOY"),
     type_external_source: z.string().optional().default(""),
     porter_app_id: z.number(),
-    metadata: porterAppDeployEventMetadataValidator,
+    image_tag: z.string().optional(),
+    app_revision_id: z.string(),
+    rollback_target_app_revision_id: z.string().optional(),
+    rollback_target_image_tag: z.string().optional(),
+    service_deployment_metadata: z
+        .record(
+            z.object({
+              status: z.string(),
+              type: z.string(),
+            })
+        )
+        .optional(),
+    end_time: z.string().optional(),
   }),
   z.object({
     id: z.string(),

+ 2 - 0
internal/models/app_revision.go

@@ -84,4 +84,6 @@ type AppRevision struct {
 
 	// AppInstanceID is the ID of the AppInstance that the revision belongs to. This will be null while the app instance table is being seeded (tracking: POR-1991)
 	AppInstanceID uuid.UUID `json:"app_instance_id" gorm:"type:uuid;default:00000000-0000-0000-0000-000000000000"`
+
+	Metadata JSONB `json:"metadata" sql:"type:jsonb" gorm:"type:jsonb"`
 }

+ 2 - 0
internal/repository/app_instance.go

@@ -10,4 +10,6 @@ import (
 type AppInstanceRepository interface {
 	// Get returns an app instance by its id
 	Get(ctx context.Context, id string) (*models.AppInstance, error)
+	// FromNameAndDeploymentTargetId returns an app instance by its name and deployment target id
+	FromNameAndDeploymentTargetId(ctx context.Context, name, deploymentTargetId string) (*models.AppInstance, error)
 }

+ 3 - 0
internal/repository/app_revision.go

@@ -2,6 +2,7 @@ package repository
 
 import (
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository/gorm/helpers"
 )
 
 // AppRevisionRepository represents the set of queries on the AppRevision model
@@ -10,4 +11,6 @@ type AppRevisionRepository interface {
 	AppRevisionByInstanceIDAndRevisionNumber(projectID uint, appInstanceId string, revisionNumber uint) (*models.AppRevision, error)
 	// LatestNumberedAppRevision finds the latest numbered app revision
 	LatestNumberedAppRevision(projectID uint, appInstanceId string) (*models.AppRevision, error)
+	// ListNumberedRevisions lists all numbered revisions for an app instance
+	Revisions(projectID uint, appInstanceId string, opts ...helpers.QueryOption) ([]*models.AppRevision, helpers.PaginatedResult, error)
 }

+ 19 - 0
internal/repository/gorm/app_instance.go

@@ -38,3 +38,22 @@ func (repo *AppInstanceRepository) Get(ctx context.Context, id string) (*models.
 
 	return appInstance, nil
 }
+
+// FromNameAndDeploymentTargetId returns an app instance by its name and deployment target id
+func (repo *AppInstanceRepository) FromNameAndDeploymentTargetId(ctx context.Context, name string, deploymentTargetId string) (*models.AppInstance, error) {
+	ctx, span := telemetry.NewSpan(ctx, "from-name-and-deployment-target-id")
+	defer span.End()
+
+	appInstance := &models.AppInstance{}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "name", Value: name},
+		telemetry.AttributeKV{Key: "deployment", Value: deploymentTargetId},
+	)
+
+	if err := repo.db.Where("name = ? and deployment_target_id = ?", name, deploymentTargetId).First(&appInstance).Error; err != nil {
+		return nil, telemetry.Error(ctx, span, err, "error getting app instance")
+	}
+
+	return appInstance, nil
+}

+ 17 - 0
internal/repository/gorm/app_revision.go

@@ -3,6 +3,7 @@ package gorm
 import (
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/repository/gorm/helpers"
 	"gorm.io/gorm"
 )
 
@@ -38,3 +39,19 @@ func (repo *AppRevisionRepository) LatestNumberedAppRevision(projectID uint, app
 
 	return AppRevision, nil
 }
+
+// Revisions lists all revisions for a projectID and appInstanceId
+func (repo *AppRevisionRepository) Revisions(projectID uint, appInstanceId string, opts ...helpers.QueryOption) ([]*models.AppRevision, helpers.PaginatedResult, error) {
+	var appRevisions []*models.AppRevision
+
+	resultDB := repo.db.Where("project_id = ? AND app_instance_id = ?", projectID, appInstanceId)
+
+	var paginatedResult helpers.PaginatedResult
+	resultDB = resultDB.Scopes(helpers.Paginate(repo.db, &paginatedResult, opts...))
+
+	if err := resultDB.Find(&appRevisions).Error; err != nil {
+		return nil, paginatedResult, err
+	}
+
+	return appRevisions, paginatedResult, nil
+}