Jelajahi Sumber

POR-1707 update revision when build fails (#3574)

ianedwards 2 tahun lalu
induk
melakukan
91885cb599

+ 26 - 0
api/client/porter_app.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	"github.com/porter-dev/porter/api/server/handlers/porter_app"
+	"github.com/porter-dev/porter/internal/models"
 
 	"github.com/porter-dev/porter/api/types"
 )
@@ -387,3 +388,28 @@ func (c *Client) PredeployStatus(
 
 	return resp, err
 }
+
+// UpdateRevisionStatus updates the status of an app revision
+func (c *Client) UpdateRevisionStatus(
+	ctx context.Context,
+	projectID uint, clusterID uint,
+	appName string, appRevisionId string,
+	status models.AppRevisionStatus,
+) (*porter_app.UpdateAppRevisionStatusResponse, error) {
+	resp := &porter_app.UpdateAppRevisionStatusResponse{}
+
+	req := &porter_app.UpdateAppRevisionStatusRequest{
+		Status: status,
+	}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/%s/revisions/%s",
+			projectID, clusterID, appName, appRevisionId,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}

+ 107 - 0
api/server/handlers/porter_app/update_app_revision_status.go

@@ -0,0 +1,107 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"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/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/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// UpdateAppRevisionStatusHandler handles requests to the /apps/{porter_app_name}/revisions/{app_revision_id} endpoint
+type UpdateAppRevisionStatusHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewUpdateAppRevisionStatusHandler returns a new UpdateAppRevisionStatusHandler
+func NewUpdateAppRevisionStatusHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdateAppRevisionStatusHandler {
+	return &UpdateAppRevisionStatusHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// UpdateAppRevisionStatusRequest is the request object for the /apps/{porter_app_name}/revisions/{app_revision_id} endpoint
+type UpdateAppRevisionStatusRequest struct {
+	// Status is the new status to set for the app revision
+	Status models.AppRevisionStatus `json:"status"`
+	// AppRevisionID is the ID of the app revision to update
+	AppRevisionID string `json:"app_revision_id"`
+}
+
+// UpdateAppRevisionStatusResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id} endpoint
+type UpdateAppRevisionStatusResponse struct{}
+
+// UpdateAppRevisionStatus updates the status of an app revision
+func (c *UpdateAppRevisionStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-update-app-revision-status")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	// read the request object from the decoder
+	request := &UpdateAppRevisionStatusRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if request.Status == "" {
+		err := telemetry.Error(ctx, span, nil, "status cannot be empty")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appRevisionID, err := uuid.Parse(request.AppRevisionID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error parsing app revision id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if appRevisionID == uuid.Nil {
+		err := telemetry.Error(ctx, span, nil, "app revision id cannot be nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	var statusProto porterv1.EnumRevisionStatus
+	switch request.Status {
+	case models.AppRevisionStatus_BuildFailed:
+		statusProto = porterv1.EnumRevisionStatus_ENUM_REVISION_STATUS_BUILD_FAILED
+	case models.AppRevisionStatus_DeployFailed:
+		statusProto = porterv1.EnumRevisionStatus_ENUM_REVISION_STATUS_DEPLOY_FAILED
+	case models.AppRevisionStatus_PredeployFailed:
+		statusProto = porterv1.EnumRevisionStatus_ENUM_REVISION_STATUS_PREDEPLOY_FAILED
+	default:
+		err := telemetry.Error(ctx, span, nil, "invalid status")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	updateStatusReq := connect.NewRequest(&porterv1.UpdateRevisionStatusRequest{
+		ProjectId:      int64(project.ID),
+		AppRevisionId:  appRevisionID.String(),
+		RevisionStatus: statusProto,
+	})
+
+	_, err = c.Config().ClusterControlPlaneClient.UpdateRevisionStatus(ctx, updateStatusReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error updating revision status")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	res := &UpdateAppRevisionStatusResponse{}
+	c.WriteResult(w, r, res)
+}

+ 29 - 0
api/server/router/porter_app.go

@@ -949,5 +949,34 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/metrics -> porter_app.NewUpdateAppRevisionStatusHandler
+	updateAppRevisionStatusEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("/apps/{%s}/revisions/{%s}", types.URLParamPorterAppName, types.URLParamAppRevisionID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	updateAppRevisionStatusHandler := porter_app.NewUpdateAppRevisionStatusHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updateAppRevisionStatusEndpoint,
+		Handler:  updateAppRevisionStatusHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 2 - 0
cli/cmd/v2/apply.go

@@ -12,6 +12,7 @@ import (
 
 	"github.com/porter-dev/porter/api/server/handlers/porter_app"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
 
 	"github.com/cli/cli/git"
 
@@ -144,6 +145,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por
 
 		if err != nil {
 			_ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, eventID, types.PorterAppEventStatus_Failed, buildMetadata)
+			_, _ = client.UpdateRevisionStatus(ctx, cliConf.Project, cliConf.Cluster, appName, applyResp.AppRevisionId, models.AppRevisionStatus_BuildFailed)
 			return fmt.Errorf("error building app: %w", err)
 		}
 

+ 2 - 0
cli/cmd/v2/build.go

@@ -37,6 +37,8 @@ type buildInput struct {
 	// CurrentImageTag is used in docker build to cache from
 	CurrentImageTag string
 	RepositoryURL   string
+
+	Env map[string]string
 }
 
 // build will create an image repository if it does not exist, and then build and push the image

+ 24 - 1
internal/models/app_revision.go

@@ -5,6 +5,29 @@ import (
 	"gorm.io/gorm"
 )
 
+// AppRevisionStatus is the status of an app revision
+type AppRevisionStatus string
+
+const (
+	// AppRevisionStatus_Created is the initial status for a revision when first inserted in database
+	AppRevisionStatus_Created AppRevisionStatus = "CREATED"
+	// AppRevisionStatus_AwaitingBuild is the status for a revision that still needs to be built
+	AppRevisionStatus_AwaitingBuild AppRevisionStatus = "AWAITING_BUILD_ARTIFACT"
+	// AppRevisionStatus_AwaitingPredeploy is the status for a revision that is waiting for a predeploy to complete.
+	AppRevisionStatus_AwaitingPredeploy AppRevisionStatus = "AWAITING_PREDEPLOY"
+	// AppRevisionStatus_Deployed is the status for a revision that has been deployed
+	AppRevisionStatus_Deployed AppRevisionStatus = "DEPLOYED"
+
+	// AppRevisionStatus_BuildCanceled is the status for a revision that was canceled during the build process
+	AppRevisionStatus_BuildCanceled AppRevisionStatus = "BUILD_CANCELED"
+	// AppRevisionStatus_BuildFailed is the status for a revision that failed to build
+	AppRevisionStatus_BuildFailed AppRevisionStatus = "BUILD_FAILED"
+	// AppRevisionStatus_PredeployFailed is the status for a revision that failed to predeploy
+	AppRevisionStatus_PredeployFailed AppRevisionStatus = "PREDEPLOY_FAILED"
+	// AppRevisionStatus_DeployFailed is the status for a revision that failed to deploy
+	AppRevisionStatus_DeployFailed AppRevisionStatus = "DEPLOY_FAILED"
+)
+
 // AppRevision represents the full spec for a revision of a porter app
 type AppRevision struct {
 	gorm.Model
@@ -16,7 +39,7 @@ type AppRevision struct {
 	Base64App string `json:"base64_app"`
 
 	// Status is the status of the apply that happened for this revision.
-	Status string `json:"status"`
+	Status AppRevisionStatus `json:"status"`
 
 	// DeploymentTargetID is the ID of the deployment target that the revision applies to.
 	DeploymentTargetID uuid.UUID `json:"deployment_target_id"`