Browse Source

check app revision status in update flow (#4063)

Feroze Mohideen 2 năm trước cách đây
mục cha
commit
3954cb3de2

+ 20 - 0
api/client/porter_app.go

@@ -479,6 +479,26 @@ func (c *Client) GetRevision(
 	return resp, err
 }
 
+// GetRevisionStatus returns the status of an app revision
+func (c *Client) GetRevisionStatus(
+	ctx context.Context,
+	projectID uint, clusterID uint,
+	appName string, appRevisionId string,
+) (*porter_app.GetAppRevisionStatusResponse, error) {
+	resp := &porter_app.GetAppRevisionStatusResponse{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/%s/revisions/%s/status",
+			projectID, clusterID, appName, appRevisionId,
+		),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}
+
 // UpdateRevisionStatus updates the status of an app revision
 func (c *Client) UpdateRevisionStatus(
 	ctx context.Context,

+ 92 - 0
api/server/handlers/porter_app/get_app_revision_status.go

@@ -0,0 +1,92 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	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"
+)
+
+// GetAppRevisionStatusHandler handles requests to the /apps/{porter_app_name}/revisions/{app_revision_id}/status endpoint
+type GetAppRevisionStatusHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewGetAppRevisionStatusHandler returns a new GetAppRevisionStatusHandler
+func NewGetAppRevisionStatusHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetAppRevisionHandler {
+	return &GetAppRevisionHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// GetAppRevisionStatusResponse represents the response from the /apps/{porter_app_name}/revisions/{app_revision_id}/status endpoint
+type GetAppRevisionStatusResponse struct {
+	AppRevisionStatus porter_app.RevisionStatus `json:"app_revision_status"`
+}
+
+// GetAppRevisionStatusHandler returns the status of an app revision
+func (c *GetAppRevisionStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-app-revision")
+	defer span.End()
+
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	appRevisionID, reqErr := requestutils.GetURLParamString(r, types.URLParamAppRevisionID)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing app revision id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	getRevisionStatusReq := connect.NewRequest(&porterv1.AppRevisionStatusRequest{
+		ProjectId:     int64(project.ID),
+		AppRevisionId: appRevisionID,
+	})
+	ccpResp, err := c.Config().ClusterControlPlaneClient.AppRevisionStatus(ctx, getRevisionStatusReq)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting app revision status")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if ccpResp == nil || ccpResp.Msg == nil {
+		err = telemetry.Error(ctx, span, nil, "get app revision response is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	revisionStatus := porter_app.RevisionStatus{
+		PredeployStarted:     ccpResp.Msg.PredeployStarted,
+		PredeploySuccessful:  ccpResp.Msg.PredeploySuccessful,
+		PredeployFailed:      ccpResp.Msg.PredeployFailed,
+		InstallStarted:       ccpResp.Msg.InstallStarted,
+		InstallSuccessful:    ccpResp.Msg.InstallSuccessful,
+		InstallFailed:        ccpResp.Msg.InstallFailed,
+		DeploymentStarted:    ccpResp.Msg.DeploymentStarted,
+		DeploymentSuccessful: ccpResp.Msg.DeploymentSuccessful,
+		DeploymentFailed:     ccpResp.Msg.DeploymentFailed,
+		IsInTerminalStatus:   ccpResp.Msg.IsInTerminalStatus,
+	}
+
+	res := &GetAppRevisionStatusResponse{
+		AppRevisionStatus: revisionStatus,
+	}
+
+	c.WriteResult(w, r, res)
+}

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

@@ -1212,6 +1212,35 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id}/status -> porter_app.NewGetAppRevisionStatusHandler
+	getAppRevisionStatusEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("/apps/{%s}/revisions/{%s}/status", types.URLParamPorterAppName, types.URLParamAppRevisionID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getAppRevisionStatusHandler := porter_app.NewGetAppRevisionStatusHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getAppRevisionStatusEndpoint,
+		Handler:  getAppRevisionStatusHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id} -> porter_app.NewUpdateAppRevisionStatusHandler
 	updateAppRevisionStatusEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 17 - 14
cli/cmd/v2/update.go

@@ -202,31 +202,34 @@ func Update(ctx context.Context, inp UpdateInput) error {
 
 	now := time.Now().UTC()
 
-	var status models.AppRevisionStatus
+	var status *porter_app.GetAppRevisionStatusResponse
 
 	for {
 		if time.Since(now) > checkDeployTimeout {
 			return errors.New("timed out waiting for app to deploy")
 		}
 
-		revision, err := client.GetRevision(ctx, cliConf.Project, cliConf.Cluster, appName, updateResp.AppRevisionId)
+		status, err := client.GetRevisionStatus(ctx, cliConf.Project, cliConf.Cluster, appName, updateResp.AppRevisionId)
 		if err != nil {
 			return fmt.Errorf("error getting app revision status: %w", err)
 		}
-		status = revision.AppRevision.Status
-
-		if status == models.AppRevisionStatus_PredeployFailed ||
-			status == models.AppRevisionStatus_InstallFailed ||
-			status == models.AppRevisionStatus_InstallSuccessful ||
-			status == models.AppRevisionStatus_DeploymentSuccessful ||
-			status == models.AppRevisionStatus_DeploymentProgressing ||
-			status == models.AppRevisionStatus_DeploymentFailed {
-			break
+
+		if status == nil {
+			return errors.New("unable to determine status of app revision")
 		}
-		if status == models.AppRevisionStatus_AwaitingPredeploy {
+
+		if status.AppRevisionStatus.PredeployStarted {
 			color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec
 		}
 
+		if status.AppRevisionStatus.InstallStarted {
+			color.New(color.FgGreen).Printf("Waiting for deploy to complete...\n") // nolint:errcheck,gosec
+		}
+
+		if status.AppRevisionStatus.IsInTerminalStatus {
+			break
+		}
+
 		time.Sleep(checkDeployFrequency)
 	}
 
@@ -239,10 +242,10 @@ func Update(ctx context.Context, inp UpdateInput) error {
 		CommitSHA:     commitSHA,
 	})
 
-	if status == models.AppRevisionStatus_InstallFailed {
+	if status.AppRevisionStatus.InstallFailed {
 		return errors.New("app failed to deploy")
 	}
-	if status == models.AppRevisionStatus_PredeployFailed {
+	if status.AppRevisionStatus.PredeployFailed {
 		return errors.New("predeploy failed for new revision")
 	}
 

+ 1 - 1
go.mod

@@ -83,7 +83,7 @@ require (
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.2.59
+	github.com/porter-dev/api-contracts v0.2.64
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d

+ 2 - 0
go.sum

@@ -1522,6 +1522,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
 github.com/porter-dev/api-contracts v0.2.59 h1:EV2xr9a5FpPHnTsz77W3dV/qWC8MnE0kOD+tBZuwhvE=
 github.com/porter-dev/api-contracts v0.2.59/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.2.64 h1:qLRomQRoOWqSMo8lx/rY+jACiIeCAExog04KfgPlXxY=
+github.com/porter-dev/api-contracts v0.2.64/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 24 - 0
internal/porter_app/revisions.go

@@ -41,6 +41,30 @@ type Revision struct {
 	AppInstanceID uuid.UUID `json:"app_instance_id"`
 }
 
+// RevisionStatus describes the status of a revision
+type RevisionStatus struct {
+	// PredeployStarted is true if the predeploy process has started
+	PredeployStarted bool `json:"predeploy_started"`
+	// PredeploySuccessful is true if the predeploy process has completed successfully
+	PredeploySuccessful bool `json:"predeploy_successful"`
+	// PredeployFailed is true if the predeploy process has failed
+	PredeployFailed bool `json:"predeploy_failed"`
+	// InstallStarted is true if the install process has started
+	InstallStarted bool `json:"install_started"`
+	// InstallSuccessful is true if the install process has completed successfully
+	InstallSuccessful bool `json:"install_successful"`
+	// InstallFailed is true if the install process has failed
+	InstallFailed bool `json:"install_failed"`
+	// DeploymentStarted is true if the deployment process has started
+	DeploymentStarted bool `json:"deployment_started"`
+	// DeploymentSuccessful is true if the deployment process has completed successfully
+	DeploymentSuccessful bool `json:"deployment_successful"`
+	// DeploymentFailed is true if the deployment process has failed
+	DeploymentFailed bool `json:"deployment_failed"`
+	// IsInTerminalStatus is true if the revision is in a terminal status
+	IsInTerminalStatus bool `json:"is_in_terminal_status"`
+}
+
 // DeploymentTarget is a simplified version of the deployment target struct
 type DeploymentTarget struct {
 	ID   string `json:"id"`