2
0
Эх сурвалжийг харах

[POR-1799] Porter app update-tag in porter.yaml v2 (#3640)

Co-authored-by: d-g-town <66391417+d-g-town@users.noreply.github.com>
Feroze Mohideen 2 жил өмнө
parent
commit
fd170f21c4

+ 25 - 0
api/client/porter_app.go

@@ -484,3 +484,28 @@ func (c *Client) PorterYamlV2Pods(
 
 	return resp, err
 }
+
+// UpdateImage updates the image for a porter app (porter yaml v2 only)
+func (c *Client) UpdateImage(
+	ctx context.Context,
+	projectID, clusterID uint,
+	appName, deploymentTargetId, tag string,
+) (*porter_app.UpdateImageResponse, error) {
+	req := &porter_app.UpdateImageRequest{
+		Tag:                tag,
+		DeploymentTargetId: deploymentTargetId,
+	}
+
+	resp := &porter_app.UpdateImageResponse{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/%s/update-image",
+			projectID, clusterID, appName,
+		),
+		&req,
+		resp,
+	)
+
+	return resp, err
+}

+ 97 - 0
api/server/handlers/porter_app/update_image.go

@@ -0,0 +1,97 @@
+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/telemetry"
+)
+
+// UpdateImageHandler is the handler for the /apps/{porter_app_name}/update-image endpoint
+type UpdateImageHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewUpdateImageHandler handles POST requests to the /apps/{porter_app_name}/update-image endpoint
+func NewUpdateImageHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdateImageHandler {
+	return &UpdateImageHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// UpdateImageRequest is the request object for the /apps/{porter_app_name}/update-image endpoint
+type UpdateImageRequest struct {
+	DeploymentTargetId string `json:"deployment_target_id"`
+	Repository         string `json:"repository"`
+	Tag                string `json:"tag"`
+}
+
+// UpdateImageResponse is the response object for the /apps/{porter_app_name}/update-image endpoint
+type UpdateImageResponse struct {
+	Repository string `json:"repository"`
+	Tag        string `json:"tag"`
+}
+
+func (c *UpdateImageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-update-image")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
+		err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
+		return
+	}
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing app name")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName})
+
+	request := &UpdateImageRequest{}
+	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
+	}
+
+	updateImageReq := connect.NewRequest(&porterv1.UpdateAppImageRequest{
+		ProjectId:          int64(project.ID),
+		DeploymentTargetId: request.DeploymentTargetId,
+		RepositoryUrl:      request.Repository,
+		Tag:                request.Tag,
+		AppName:            appName,
+	})
+	ccpResp, err := c.Config().ClusterControlPlaneClient.UpdateAppImage(ctx, updateImageReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error calling ccp update porter app image")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	res := &UpdateImageResponse{
+		Repository: ccpResp.Msg.RepositoryUrl,
+		Tag:        ccpResp.Msg.Tag,
+	}
+
+	c.WriteResult(w, r, res)
+}

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

@@ -688,6 +688,35 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/update-image -> porter_app.NewUpdateImageHandler
+	updatePorterAppImageEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/update-image", relPathV2, types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	updatePorterAppImageHandler := porter_app.NewUpdateImageHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updatePorterAppImageEndpoint,
+		Handler:  updatePorterAppImageHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/default-deployment-target -> porter_app.NewDefaultDeploymentTargetHandler
 	defaultDeploymentTargetEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 50 - 35
cli/cmd/commands/app.go

@@ -14,6 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/utils"
+	v2 "github.com/porter-dev/porter/cli/cmd/v2"
 	"github.com/spf13/cobra"
 	batchv1 "k8s.io/api/batch/v1"
 	v1 "k8s.io/api/core/v1"
@@ -353,7 +354,7 @@ func appCleanup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, clie
 	}
 
 	for _, podName := range selectedPods {
-		color.New(color.FgBlue).Printf("Deleting ephemeral pod: %s\n", podName)
+		_, _ = color.New(color.FgBlue).Printf("Deleting ephemeral pod: %s\n", podName)
 
 		err = config.Clientset.CoreV1().Pods(appNamespace).Delete(
 			ctx, podName, metav1.DeleteOptions{},
@@ -602,7 +603,7 @@ func appExecuteRunEphemeral(ctx context.Context, config *AppPorterRunSharedConfi
 	// delete the ephemeral pod no matter what
 	defer appDeletePod(ctx, config, podName, namespace) //nolint:errcheck,gosec // do not want to change logic of CLI. New linter error
 
-	color.New(color.FgYellow).Printf("Waiting for pod %s to be ready...", podName)
+	_, _ = color.New(color.FgYellow).Printf("Waiting for pod %s to be ready...", podName)
 	if err = appWaitForPod(ctx, config, newPod); err != nil {
 		color.New(color.FgRed).Println("failed")
 		return appHandlePodAttachError(ctx, err, config, namespace, podName, container)
@@ -1145,44 +1146,58 @@ func appCreateEphemeralPodFromExisting(
 }
 
 func appUpdateTag(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, args []string) error {
-	namespace := fmt.Sprintf("porter-stack-%s", args[0])
-	if appTag == "" {
-		appTag = "latest"
-	}
-	release, err := client.GetRelease(ctx, cliConfig.Project, cliConfig.Cluster, namespace, args[0])
+	project, err := client.GetProject(ctx, cliConfig.Project)
 	if err != nil {
-		return fmt.Errorf("Unable to find application %s", args[0])
-	}
-	repository, ok := release.Config["global"].(map[string]interface{})["image"].(map[string]interface{})["repository"].(string)
-	if !ok || repository == "" {
-		return fmt.Errorf("Application %s does not have an associated image repository. Unable to update tag", args[0])
-	}
-	imageInfo := types.ImageInfo{
-		Repository: repository,
-		Tag:        appTag,
-	}
-	createUpdatePorterAppRequest := &types.CreatePorterAppRequest{
-		ClusterID:       cliConfig.Cluster,
-		ProjectID:       cliConfig.Project,
-		ImageInfo:       imageInfo,
-		OverrideRelease: false,
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
 	}
 
-	color.New(color.FgGreen).Printf("Updating application %s to build using tag \"%s\"\n", args[0], appTag)
+	if project.ValidateApplyV2 {
+		tag, err := v2.UpdateImage(ctx, appTag, client, cliConfig.Project, cliConfig.Cluster, args[0])
+		if err != nil {
+			return fmt.Errorf("error updating tag: %w", err)
+		}
+		_, _ = color.New(color.FgGreen).Printf("Successfully updated application %s to use tag \"%s\"\n", args[0], tag)
+		return nil
+	} else {
+		namespace := fmt.Sprintf("porter-stack-%s", args[0])
+		if appTag == "" {
+			appTag = "latest"
+		}
+		release, err := client.GetRelease(ctx, cliConfig.Project, cliConfig.Cluster, namespace, args[0])
+		if err != nil {
+			return fmt.Errorf("Unable to find application %s", args[0])
+		}
+		repository, ok := release.Config["global"].(map[string]interface{})["image"].(map[string]interface{})["repository"].(string)
+		if !ok || repository == "" {
+			return fmt.Errorf("Application %s does not have an associated image repository. Unable to update tag", args[0])
+		}
+		imageInfo := types.ImageInfo{
+			Repository: repository,
+			Tag:        appTag,
+		}
+		createUpdatePorterAppRequest := &types.CreatePorterAppRequest{
+			ClusterID:       cliConfig.Cluster,
+			ProjectID:       cliConfig.Project,
+			ImageInfo:       imageInfo,
+			OverrideRelease: false,
+		}
+
+		_, _ = color.New(color.FgGreen).Printf("Updating application %s to build using tag \"%s\"\n", args[0], appTag)
 
-	_, err = client.CreatePorterApp(
-		ctx,
-		cliConfig.Project,
-		cliConfig.Cluster,
-		args[0],
-		createUpdatePorterAppRequest,
-	)
-	if err != nil {
-		return fmt.Errorf("Unable to update application %s: %w", args[0], err)
-	}
+		_, err = client.CreatePorterApp(
+			ctx,
+			cliConfig.Project,
+			cliConfig.Cluster,
+			args[0],
+			createUpdatePorterAppRequest,
+		)
+		if err != nil {
+			return fmt.Errorf("Unable to update application %s: %w", args[0], err)
+		}
 
-	color.New(color.FgGreen).Printf("Successfully updated application %s to use tag \"%s\"\n", args[0], appTag)
-	return nil
+		_, _ = color.New(color.FgGreen).Printf("Successfully updated application %s to use tag \"%s\"\n", args[0], appTag)
+		return nil
+	}
 }
 
 func getPodsFromV1PorterYaml(ctx context.Context, execArgs []string, client api.Client, cliConfig config.CLIConfig, porterAppName string, namespace string) ([]appPodSimple, []string, error) {

+ 32 - 0
cli/cmd/v2/update_image.go

@@ -0,0 +1,32 @@
+package v2
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	api "github.com/porter-dev/porter/api/client"
+)
+
+// UpdateImage updates the image of an application
+func UpdateImage(ctx context.Context, tag string, client api.Client, projectId, clusterId uint, appName string) (string, error) {
+	targetResp, err := client.DefaultDeploymentTarget(ctx, projectId, clusterId)
+	if err != nil {
+		return "", fmt.Errorf("error calling default deployment target endpoint: %w", err)
+	}
+
+	if targetResp.DeploymentTargetID == "" {
+		return "", errors.New("deployment target id is empty")
+	}
+
+	if tag == "" {
+		tag = "latest"
+	}
+
+	resp, err := client.UpdateImage(ctx, projectId, clusterId, appName, targetResp.DeploymentTargetID, tag)
+	if err != nil {
+		return "", fmt.Errorf("unable to update image: %w", err)
+	}
+
+	return resp.Tag, nil
+}

+ 1 - 1
go.mod

@@ -82,7 +82,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.1.8
+	github.com/porter-dev/api-contracts v0.1.9
 	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 - 2
go.sum

@@ -1516,8 +1516,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.1.8 h1:g8qq2TeN6W6T+FgQfv7RP/sDEFE2CxhK1sm6C4q78e8=
-github.com/porter-dev/api-contracts v0.1.8/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.1.9 h1:EGNZjVjBKPIP+w7fcMhi3njWEt1V1kiK8cd2h87vFQk=
+github.com/porter-dev/api-contracts v0.1.9/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=