From e12ff9a1880d564d94f387d3dc755455f2b44fa7 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Fri, 15 Sep 2023 11:46:47 -0400 Subject: [PATCH] POR-1728 Build env vars (#3576) --- api/client/porter_app.go | 24 +++ .../handlers/porter_app/get_build_env.go | 151 ++++++++++++++++++ .../porter_app/update_app_revision_status.go | 19 ++- api/server/router/porter_app.go | 31 +++- cli/cmd/v2/apply.go | 6 + cli/cmd/v2/build.go | 2 + internal/models/deployment_target.go | 10 +- internal/porter_app/environment.go | 82 ++++++++++ 8 files changed, 313 insertions(+), 12 deletions(-) create mode 100644 api/server/handlers/porter_app/get_build_env.go create mode 100644 internal/porter_app/environment.go diff --git a/api/client/porter_app.go b/api/client/porter_app.go index b4931c0adb..6789ccb153 100644 --- a/api/client/porter_app.go +++ b/api/client/porter_app.go @@ -413,3 +413,27 @@ func (c *Client) UpdateRevisionStatus( return resp, err } + +// GetBuildEnv returns the build environment for a given app proto +func (c *Client) GetBuildEnv( + ctx context.Context, + projectID uint, clusterID uint, + base64AppProto string, +) (*porter_app.GetBuildEnvResponse, error) { + resp := &porter_app.GetBuildEnvResponse{} + + req := &porter_app.GetBuildEnvRequest{ + Base64AppProto: base64AppProto, + } + + err := c.postRequest( + fmt.Sprintf( + "/projects/%d/clusters/%d/apps/build-env", + projectID, clusterID, + ), + req, + resp, + ) + + return resp, err +} diff --git a/api/server/handlers/porter_app/get_build_env.go b/api/server/handlers/porter_app/get_build_env.go new file mode 100644 index 0000000000..520d914f06 --- /dev/null +++ b/api/server/handlers/porter_app/get_build_env.go @@ -0,0 +1,151 @@ +package porter_app + +import ( + "encoding/base64" + "net/http" + + "github.com/porter-dev/api-contracts/generated/go/helpers" + 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/types" + "github.com/porter-dev/porter/internal/models" + "github.com/porter-dev/porter/internal/porter_app" + "github.com/porter-dev/porter/internal/telemetry" +) + +// GetBuildEnvHandler is the handler for the /apps/build-env endpoint +type GetBuildEnvHandler struct { + handlers.PorterHandlerReadWriter + authz.KubernetesAgentGetter +} + +// NewGetBuildEnvHandler handles GET requests to the /apps/build-env endpoint +func NewGetBuildEnvHandler( + config *config.Config, + decoderValidator shared.RequestDecoderValidator, + writer shared.ResultWriter, +) *GetBuildEnvHandler { + return &GetBuildEnvHandler{ + PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer), + KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config), + } +} + +// GetBuildEnvRequest is the request object for the /apps/build-env endpoint +type GetBuildEnvRequest struct { + Base64AppProto string `json:"b64_app_proto"` +} + +// GetBuildEnvResponse is the response object for the /apps/build-env endpoint +type GetBuildEnvResponse struct { + BuildEnvVariables map[string]string `json:"build_env_variables"` +} + +// ServeHTTP translates the request into a GetBuildEnvRequest request, uses the proto to query the cluster for the build env, and returns the response +func (c *GetBuildEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx, span := telemetry.NewSpan(r.Context(), "serve-get-build-env") + defer span.End() + + project, _ := ctx.Value(types.ProjectScope).(*models.Project) + cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster) + + telemetry.WithAttributes(span, + telemetry.AttributeKV{Key: "project-id", Value: project.ID}, + telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID}, + ) + + 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.NewErrForbidden(err)) + return + } + + request := &ApplyPorterAppRequest{} + 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.Base64AppProto == "" { + err := telemetry.Error(ctx, span, nil, "app proto is empty") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + + decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto) + if err != nil { + err := telemetry.Error(ctx, span, err, "error decoding base yaml") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + + appProto := &porterv1.PorterApp{} + err = helpers.UnmarshalContractObject(decoded, appProto) + if err != nil { + err := telemetry.Error(ctx, span, err, "error unmarshalling app proto") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + + deploymentTargets, err := c.Repo().DeploymentTarget().List(project.ID) + if err != nil { + err = telemetry.Error(ctx, span, err, "error reading deployment targets") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + if len(deploymentTargets) == 0 { + err := telemetry.Error(ctx, span, nil, "no deployment targets found") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + if len(deploymentTargets) > 1 { + err = telemetry.Error(ctx, span, nil, "more than one deployment target found") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + deploymentTarget := deploymentTargets[0] + if deploymentTarget.ClusterID != int(cluster.ID) { + err := telemetry.Error(ctx, span, nil, "deployment target does not belong to cluster") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + agent, err := c.GetAgent(r, cluster, "") + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting agent") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + envFromProtoInp := porter_app.AppEnvironmentFromProtoInput{ + App: appProto, + DeploymentTarget: deploymentTarget, + K8SAgent: agent, + } + envGroups, err := porter_app.AppEnvironmentFromProto(ctx, envFromProtoInp) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting app environment from revision") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + buildEnvVariables := make(map[string]string) + for _, envGroup := range envGroups { + for key, val := range envGroup.Variables { + buildEnvVariables[key] = val + } + } + + res := &GetBuildEnvResponse{ + BuildEnvVariables: buildEnvVariables, + } + + c.WriteResult(w, r, res) +} diff --git a/api/server/handlers/porter_app/update_app_revision_status.go b/api/server/handlers/porter_app/update_app_revision_status.go index 5bac846a3c..e9541faa44 100644 --- a/api/server/handlers/porter_app/update_app_revision_status.go +++ b/api/server/handlers/porter_app/update_app_revision_status.go @@ -4,12 +4,12 @@ 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/server/shared/requestutils" "github.com/porter-dev/porter/api/types" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/telemetry" @@ -35,8 +35,6 @@ func NewUpdateAppRevisionStatusHandler( 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 @@ -63,14 +61,15 @@ func (c *UpdateAppRevisionStatusHandler) ServeHTTP(w http.ResponseWriter, r *htt return } - appRevisionID, err := uuid.Parse(request.AppRevisionID) - if err != nil { - err := telemetry.Error(ctx, span, err, "error parsing app revision id") + appRevisionId, _ := requestutils.GetURLParamString(r, types.URLParamAppRevisionID) + if appRevisionId == "" { + err := telemetry.Error(ctx, span, nil, "app revision id is empty") 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") + + if appRevisionId == "" { + err := telemetry.Error(ctx, span, nil, "app revision id is empty") c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) return } @@ -91,11 +90,11 @@ func (c *UpdateAppRevisionStatusHandler) ServeHTTP(w http.ResponseWriter, r *htt updateStatusReq := connect.NewRequest(&porterv1.UpdateRevisionStatusRequest{ ProjectId: int64(project.ID), - AppRevisionId: appRevisionID.String(), + AppRevisionId: appRevisionId, RevisionStatus: statusProto, }) - _, err = c.Config().ClusterControlPlaneClient.UpdateRevisionStatus(ctx, updateStatusReq) + _, 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)) diff --git a/api/server/router/porter_app.go b/api/server/router/porter_app.go index 1e25d899cf..c61e579922 100644 --- a/api/server/router/porter_app.go +++ b/api/server/router/porter_app.go @@ -949,7 +949,7 @@ func getPorterAppRoutes( Router: r, }) - // POST /api/projects/{project_id}/clusters/{cluster_id}/apps/metrics -> porter_app.NewUpdateAppRevisionStatusHandler + // POST /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id} -> porter_app.NewUpdateAppRevisionStatusHandler updateAppRevisionStatusEndpoint := factory.NewAPIEndpoint( &types.APIRequestMetadata{ Verb: types.APIVerbUpdate, @@ -978,5 +978,34 @@ func getPorterAppRoutes( Router: r, }) + // POST /api/projects/{project_id}/clusters/{cluster_id}/apps/build-env -> porter_app.NewGetBuildEnvHandler + getBuildEnvEndpoint := factory.NewAPIEndpoint( + &types.APIRequestMetadata{ + Verb: types.APIVerbUpdate, + Method: types.HTTPVerbPost, + Path: &types.Path{ + Parent: basePath, + RelativePath: "/apps/build-env", + }, + Scopes: []types.PermissionScope{ + types.UserScope, + types.ProjectScope, + types.ClusterScope, + }, + }, + ) + + getBuildEnvHandler := porter_app.NewGetBuildEnvHandler( + config, + factory.GetDecoderValidator(), + factory.GetResultWriter(), + ) + + routes = append(routes, &router.Route{ + Endpoint: getBuildEnvEndpoint, + Handler: getBuildEnvHandler, + Router: r, + }) + return routes, newPath } diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index 1205137ba4..fc065376f9 100644 --- a/cli/cmd/v2/apply.go +++ b/cli/cmd/v2/apply.go @@ -139,6 +139,12 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por buildSettings.CurrentImageTag = currentImageTag buildSettings.ProjectID = cliConf.Project + buildEnv, err := client.GetBuildEnv(ctx, cliConf.Project, cliConf.Cluster, base64AppProto) + if err != nil { + return fmt.Errorf("error getting build env: %w", err) + } + buildSettings.Env = buildEnv.BuildEnvVariables + err = build(ctx, client, buildSettings) buildMetadata := make(map[string]interface{}) buildMetadata["end_time"] = time.Now().UTC() diff --git a/cli/cmd/v2/build.go b/cli/cmd/v2/build.go index 6b423901fe..53a63b2764 100644 --- a/cli/cmd/v2/build.go +++ b/cli/cmd/v2/build.go @@ -91,6 +91,7 @@ func build(ctx context.Context, client api.Client, inp buildInput) error { BuildContext: buildCtx, DockerfilePath: dockerfilePath, IsDockerfileInCtx: isDockerfileInCtx, + Env: inp.Env, } err = dockerAgent.BuildLocal( @@ -107,6 +108,7 @@ func build(ctx context.Context, client api.Client, inp buildInput) error { ImageRepo: imageURL, Tag: tag, BuildContext: inp.BuildContext, + Env: inp.Env, } buildConfig := &types.BuildConfig{ diff --git a/internal/models/deployment_target.go b/internal/models/deployment_target.go index 940fd92e7c..0c5070b22c 100644 --- a/internal/models/deployment_target.go +++ b/internal/models/deployment_target.go @@ -5,6 +5,14 @@ import ( "gorm.io/gorm" ) +// DeploymentTargetSelectorType is the type of selector for a deployment target +type DeploymentTargetSelectorType string + +const ( + // DeploymentTargetSelectorType_Namespace indicates that the selector is a namespace + DeploymentTargetSelectorType_Namespace DeploymentTargetSelectorType = "NAMESPACE" +) + // DeploymentTarget represents a deployment target on a given cluster type DeploymentTarget struct { gorm.Model @@ -22,5 +30,5 @@ type DeploymentTarget struct { Selector string `json:"selector"` // SelectorType is the kind of selector (i.e. NAMESPACE or LABEL). - SelectorType string `json:"selector_type"` + SelectorType DeploymentTargetSelectorType `json:"selector_type"` } diff --git a/internal/porter_app/environment.go b/internal/porter_app/environment.go new file mode 100644 index 0000000000..5dd6a32f5a --- /dev/null +++ b/internal/porter_app/environment.go @@ -0,0 +1,82 @@ +package porter_app + +import ( + "context" + + porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1" + "github.com/porter-dev/porter/internal/kubernetes" + "github.com/porter-dev/porter/internal/kubernetes/environment_groups" + "github.com/porter-dev/porter/internal/models" + "github.com/porter-dev/porter/internal/telemetry" +) + +type envVariarableOptions struct { + includeSecrets bool +} + +// EnvVariableOption is a function that modifies AppEnvironmentFromProto +type EnvVariableOption func(*envVariarableOptions) + +// WithSecrets includes secrets in the environment groups +func WithSecrets() EnvVariableOption { + return func(opts *envVariarableOptions) { + opts.includeSecrets = true + } +} + +// AppEnvironmentFromProtoInput is the input struct for AppEnvironmentFromProto +type AppEnvironmentFromProtoInput struct { + App *porterv1.PorterApp + DeploymentTarget *models.DeploymentTarget + K8SAgent *kubernetes.Agent +} + +// AppEnvironmentFromProto returns all envfironment groups referenced in an app proto with their variables +func AppEnvironmentFromProto(ctx context.Context, inp AppEnvironmentFromProtoInput, varOpts ...EnvVariableOption) ([]environment_groups.EnvironmentGroup, error) { + ctx, span := telemetry.NewSpan(ctx, "porter-app-env-from-proto") + defer span.End() + + var envGroups []environment_groups.EnvironmentGroup + + if inp.DeploymentTarget == nil { + return nil, telemetry.Error(ctx, span, nil, "must provide a deployment target") + } + if inp.K8SAgent == nil { + return nil, telemetry.Error(ctx, span, nil, "must provide a kubernetes agent") + } + if inp.App == nil { + return nil, telemetry.Error(ctx, span, nil, "must provide an app") + } + + var opts envVariarableOptions + for _, opt := range varOpts { + opt(&opts) + } + + var namespace string + switch inp.DeploymentTarget.SelectorType { + case models.DeploymentTargetSelectorType_Namespace: + namespace = inp.DeploymentTarget.Selector + default: + return envGroups, telemetry.Error(ctx, span, nil, "deployment target selector type not supported") + } + + for _, envGroupRef := range inp.App.EnvGroups { + envGroup, err := environment_groups.EnvironmentGroupInTargetNamespace(ctx, inp.K8SAgent, environment_groups.EnvironmentGroupInTargetNamespaceInput{ + Name: envGroupRef.GetName(), + Version: int(envGroupRef.GetVersion()), + Namespace: namespace, + }) + if err != nil { + return nil, err + } + + if !opts.includeSecrets { + envGroup.SecretVariables = nil + } + + envGroups = append(envGroups, envGroup) + } + + return envGroups, nil +}