Skip to content

Commit

Permalink
[POR-1799] Porter app update-tag in porter.yaml v2 (#3640)
Browse files Browse the repository at this point in the history
Co-authored-by: d-g-town <[email protected]>
  • Loading branch information
Feroze Mohideen and d-g-town authored Sep 25, 2023
1 parent 91a1d8b commit fd170f2
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 38 deletions.
25 changes: 25 additions & 0 deletions api/client/porter_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 97 additions & 0 deletions api/server/handlers/porter_app/update_image.go
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions api/server/router/porter_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
85 changes: 50 additions & 35 deletions cli/cmd/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 [email protected]")
}

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) {
Expand Down
32 changes: 32 additions & 0 deletions cli/cmd/v2/update_image.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit fd170f2

Please sign in to comment.