Skip to content

Commit

Permalink
POR-1728 Build env vars (#3576)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianedwards authored Sep 15, 2023
1 parent 91885cb commit e12ff9a
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 12 deletions.
24 changes: 24 additions & 0 deletions api/client/porter_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
151 changes: 151 additions & 0 deletions api/server/handlers/porter_app/get_build_env.go
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 9 additions & 10 deletions api/server/handlers/porter_app/update_app_revision_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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))
Expand Down
31 changes: 30 additions & 1 deletion api/server/router/porter_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
6 changes: 6 additions & 0 deletions cli/cmd/v2/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions cli/cmd/v2/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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{
Expand Down
10 changes: 9 additions & 1 deletion internal/models/deployment_target.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
}
Loading

0 comments on commit e12ff9a

Please sign in to comment.