Pārlūkot izejas kodu

implement porter app manifests command (#4122)

ianedwards 2 gadi atpakaļ
vecāks
revīzija
4f499d5376

+ 20 - 0
api/client/porter_app.go

@@ -177,6 +177,26 @@ func (c *Client) ParseYAML(
 	return resp, err
 }
 
+// GetAppManifests returns the manifests for a given app based on the latest successful app revision
+func (c *Client) GetAppManifests(
+	ctx context.Context,
+	projectID, clusterID uint,
+	appName string,
+) (*porter_app.AppManifestsResponse, error) {
+	resp := &porter_app.AppManifestsResponse{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/%s/manifests",
+			projectID, clusterID, appName,
+		),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}
+
 // ValidatePorterAppInput is the input struct to ValidatePorterApp
 type ValidatePorterAppInput struct {
 	ProjectID          uint

+ 112 - 0
api/server/handlers/porter_app/manifests.go

@@ -0,0 +1,112 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+
+	"connectrpc.com/connect"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+
+	"github.com/porter-dev/porter/internal/telemetry"
+
+	"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"
+)
+
+// AppManifestsHandler handles requests to the /apps/{porter_app_name}/manifests endpoint
+type AppManifestsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewAppManifestsHandler returns a new AppManifestsHandler
+func NewAppManifestsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *AppManifestsHandler {
+	return &AppManifestsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// AppManifestsRequest is the request object for the /apps/{porter_app_name}/manifests endpoint
+type AppManifestsRequest struct {
+	DeploymentTargetID string `schema:"deployment_target_id"`
+}
+
+// AppManifestsResponse is the response object for the /apps/{porter_app_name}/manifests endpoint
+type AppManifestsResponse struct {
+	// Base64Manifests is the base64 encoded manifests
+	Base64Manifests string `json:"base64_manifests"`
+}
+
+// ServeHTTP translates the request into a TemplateAppManifests grpc request, forwards to the cluster control plane, and returns the response.
+func (c *AppManifestsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-app-manifests")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		e := telemetry.Error(ctx, span, reqErr, "error parsing stack name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName})
+
+	request := &AppManifestsRequest{}
+	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
+	}
+
+	// optional deployment target id - if not provided, use the cluster's default
+	deploymentTargetID := request.DeploymentTargetID
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: deploymentTargetID})
+
+	var deploymentTargetIdentifer *porterv1.DeploymentTargetIdentifier
+	if deploymentTargetID != "" {
+		deploymentTargetIdentifer = &porterv1.DeploymentTargetIdentifier{
+			Id: deploymentTargetID,
+		}
+	}
+
+	appManifestsReq := connect.NewRequest(&porterv1.TemplateAppManifestsRequest{
+		ProjectId:                  int64(project.ID),
+		ClusterId:                  int64(cluster.ID),
+		AppName:                    appName,
+		DeploymentTargetIdentifier: deploymentTargetIdentifer,
+	})
+
+	appManifestsRes, err := c.Config().ClusterControlPlaneClient.TemplateAppManifests(ctx, appManifestsReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting current app manifests from cluster control plane client")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if appManifestsRes == nil || appManifestsRes.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "current app manifests resp is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	response := AppManifestsResponse{
+		Base64Manifests: appManifestsRes.Msg.Base64Manifests,
+	}
+
+	c.WriteResult(w, r, response)
+}

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

@@ -630,6 +630,35 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/manifests -> porter_app.NewGetAppManifestsHandler
+	getAppManifestsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/manifests", relPathV2, types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getAppManifestsHandler := porter_app.NewAppManifestsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getAppManifestsEndpoint,
+		Handler:  getAppManifestsHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/env-variables -> porter_app.AppEnvVariablesHandler
 	appEnvVariablesEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 36 - 0
cli/cmd/commands/app.go

@@ -2,6 +2,7 @@ package commands
 
 import (
 	"context"
+	"encoding/base64"
 	"errors"
 	"fmt"
 	"io"
@@ -129,6 +130,17 @@ func registerCommand_App(cliConf config.CLIConfig) *cobra.Command {
 	}
 	appCmd.AddCommand(appRollbackCmd)
 
+	// appManifestsCmd represents the "porter app manifest" subcommand
+	appManifestsCmd := &cobra.Command{
+		Use:   "manifests [application]",
+		Args:  cobra.MinimumNArgs(1),
+		Short: "Prints the kubernetes manifests for an application.",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return checkLoginAndRunWithConfig(cmd, cliConf, args, appManifests)
+		},
+	}
+	appCmd.AddCommand(appManifestsCmd)
+
 	return appCmd
 }
 
@@ -188,6 +200,30 @@ func appRunFlags(appRunCmd *cobra.Command) {
 	)
 }
 
+func appManifests(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ *cobra.Command, args []string) error {
+	appName := args[0]
+	if appName == "" {
+		return fmt.Errorf("app name must be specified")
+	}
+
+	manifest, err := client.GetAppManifests(ctx, cliConfig.Project, cliConfig.Cluster, appName)
+	if err != nil {
+		return fmt.Errorf("failed to get app manifest: %w", err)
+	}
+
+	decoded, err := base64.StdEncoding.DecodeString(manifest.Base64Manifests)
+	if err != nil {
+		return fmt.Errorf("failed to decode app manifest: %w", err)
+	}
+
+	_, err = os.Stdout.WriteString(string(decoded))
+	if err != nil {
+		return fmt.Errorf("failed to write app manifest: %w", err)
+	}
+
+	return nil
+}
+
 func appRollback(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ *cobra.Command, args []string) error {
 	project, err := client.GetProject(ctx, cliConfig.Project)
 	if err != nil {

+ 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.80
+	github.com/porter-dev/api-contracts v0.2.83
 	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

@@ -1520,8 +1520,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.2.80 h1:Ufa0d64kO+XhrZcANje2vltCbgn7WzsIafo3p3cu+jE=
-github.com/porter-dev/api-contracts v0.2.80/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.2.83 h1:UndRYlFMUbDa6oZaMpE8Q8sUpEfq+MOOf4pP/7MX1gw=
+github.com/porter-dev/api-contracts v0.2.83/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=