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

attach app env to revisions (#3586)

ianedwards 2 жил өмнө
parent
commit
e81b43fbd6

+ 27 - 2
api/server/handlers/porter_app/current_app_revision.go

@@ -3,6 +3,7 @@ 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"
@@ -25,6 +26,7 @@ import (
 // LatestAppRevisionHandler handles requests to the /apps/{porter_app_name}/latest endpoint
 type LatestAppRevisionHandler struct {
 	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
 }
 
 // NewLatestAppRevisionHandler returns a new LatestAppRevisionHandler
@@ -35,6 +37,7 @@ func NewLatestAppRevisionHandler(
 ) *LatestAppRevisionHandler {
 	return &LatestAppRevisionHandler{
 		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
 	}
 }
 
@@ -105,7 +108,14 @@ func (c *LatestAppRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	}
 
 	if porterApps[0].ID == 0 {
-		err := telemetry.Error(ctx, span, err, "porter app id is missiong")
+		err := telemetry.Error(ctx, span, err, "porter app id is missing")
+		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
 	}
@@ -137,8 +147,23 @@ func (c *LatestAppRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
+	revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{
+		ProjectID:                  project.ID,
+		ClusterID:                  int(cluster.ID),
+		DeploymentTargetID:         request.DeploymentTargetID,
+		Revision:                   encodedRevision,
+		K8SAgent:                   agent,
+		PorterAppRepository:        c.Repo().PorterApp(),
+		DeploymentTargetRepository: c.Repo().DeploymentTarget(),
+	})
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error attaching env to revision")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
 	response := LatestAppRevisionResponse{
-		AppRevision: encodedRevision,
+		AppRevision: revisionWithEnv,
 	}
 
 	c.WriteResult(w, r, response)

+ 5 - 28
api/server/handlers/porter_app/get_app_env.go

@@ -116,31 +116,6 @@ func (c *GetAppEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		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")
@@ -149,10 +124,12 @@ func (c *GetAppEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	envFromProtoInp := porter_app.AppEnvironmentFromProtoInput{
-		App:              appProto,
-		DeploymentTarget: deploymentTarget,
-		K8SAgent:         agent,
+		ProjectID: project.ID,
+		ClusterID: int(cluster.ID),
+		App:       appProto,
+		K8SAgent:  agent,
 	}
+
 	envGroups, err := porter_app.AppEnvironmentFromProto(ctx, envFromProtoInp, porter_app.WithEnvGroupFilter(request.EnvGroups), porter_app.WithSecrets())
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error getting app environment from revision")

+ 4 - 28
api/server/handlers/porter_app/get_build_env.go

@@ -107,31 +107,6 @@ func (c *GetBuildEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		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")
@@ -140,9 +115,10 @@ func (c *GetBuildEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	envFromProtoInp := porter_app.AppEnvironmentFromProtoInput{
-		App:              appProto,
-		DeploymentTarget: deploymentTarget,
-		K8SAgent:         agent,
+		ProjectID: project.ID,
+		ClusterID: int(cluster.ID),
+		App:       appProto,
+		K8SAgent:  agent,
 	}
 	envGroups, err := porter_app.AppEnvironmentFromProto(ctx, envFromProtoInp)
 	if err != nil {

+ 26 - 1
api/server/handlers/porter_app/list_app_revisions.go

@@ -6,6 +6,7 @@ import (
 	"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/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"
@@ -20,6 +21,7 @@ import (
 // ListAppRevisionsHandler handles requests to the /apps/{porter_app_name}/revisions endpoint
 type ListAppRevisionsHandler struct {
 	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
 }
 
 // NewListAppRevisionsHandler returns a new ListAppRevisionsHandler
@@ -30,6 +32,7 @@ func NewListAppRevisionsHandler(
 ) *ListAppRevisionsHandler {
 	return &ListAppRevisionsHandler{
 		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
 	}
 }
 
@@ -115,6 +118,13 @@ func (c *ListAppRevisionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		appRevisions = []*porterv1.AppRevision{}
 	}
 
+	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
+	}
+
 	res := &ListAppRevisionsResponse{
 		AppRevisions: make([]porter_app.Revision, 0),
 	}
@@ -127,7 +137,22 @@ func (c *ListAppRevisionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 			return
 		}
 
-		res.AppRevisions = append(res.AppRevisions, encodedRevision)
+		revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{
+			Revision:                   encodedRevision,
+			ProjectID:                  project.ID,
+			ClusterID:                  int(cluster.ID),
+			DeploymentTargetID:         request.DeploymentTargetID,
+			K8SAgent:                   agent,
+			PorterAppRepository:        c.Repo().PorterApp(),
+			DeploymentTargetRepository: c.Repo().DeploymentTarget(),
+		})
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error attaching env to revision")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		res.AppRevisions = append(res.AppRevisions, revisionWithEnv)
 	}
 
 	c.WriteResult(w, r, res)

+ 30 - 18
dashboard/src/lib/porter-apps/index.ts

@@ -70,16 +70,22 @@ export const clientAppValidator = z.object({
     readOnly: z.boolean(),
     value: z.string(),
   }),
-  envGroups: z.object({ name: z.string(), version: z.bigint() }).array().default([]),
+  envGroups: z
+    .object({ name: z.string(), version: z.bigint() })
+    .array()
+    .default([]),
   services: serviceValidator.array(),
   predeploy: serviceValidator.array().optional(),
-  env: z.object({
-    key: z.string(),
-    value: z.string(),
-    hidden: z.boolean(),
-    locked: z.boolean(),
-    deleted: z.boolean(),
-  }).array().default([]),
+  env: z
+    .object({
+      key: z.string(),
+      value: z.string(),
+      hidden: z.boolean(),
+      locked: z.boolean(),
+      deleted: z.boolean(),
+    })
+    .array()
+    .default([]),
   build: buildValidator,
 });
 export type ClientPorterApp = z.infer<typeof clientAppValidator>;
@@ -309,7 +315,10 @@ export function clientAppFromProto(
       services,
       predeploy: predeployList,
       env: [],
-      envGroups: proto.envGroups.map((eg) => ({ name: eg.name, version: eg.version })),
+      envGroups: proto.envGroups.map((eg) => ({
+        name: eg.name,
+        version: eg.version,
+      })),
       build: clientBuildFromProto(proto.build) ?? {
         method: "pack",
         context: "./",
@@ -322,15 +331,15 @@ export function clientAppFromProto(
   const predeployOverrides = serializeService(overrides.predeploy);
   const predeploy = proto.predeploy
     ? [
-      deserializeService({
-        service: serializedServiceFromProto({
-          name: "pre-deploy",
-          service: proto.predeploy,
-          isPredeploy: true,
+        deserializeService({
+          service: serializedServiceFromProto({
+            name: "pre-deploy",
+            service: proto.predeploy,
+            isPredeploy: true,
+          }),
+          override: predeployOverrides,
         }),
-        override: predeployOverrides,
-      }),
-    ]
+      ]
     : undefined;
 
   return {
@@ -341,7 +350,10 @@ export function clientAppFromProto(
     services,
     predeploy,
     env: [],
-    envGroups: proto.envGroups.map((eg) => ({ name: eg.name, version: eg.version })),
+    envGroups: proto.envGroups.map((eg) => ({
+      name: eg.name,
+      version: eg.version,
+    })),
     build: clientBuildFromProto(proto.build) ?? {
       method: "pack",
       context: "./",

+ 10 - 6
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -112,6 +112,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
           builder: "",
           buildpacks: [],
         },
+        env: [],
       },
       source: {
         git_repo_name: "",
@@ -139,6 +140,8 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
   const build = watch("app.build");
   const image = watch("source.image");
   const services = watch("app.services");
+  const env = watch("app.env");
+
   const {
     detectedServices: servicesFromYaml,
     porterYamlFound,
@@ -155,7 +158,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
   const onSubmit = handleSubmit(async (data) => {
     try {
       setDeployError("");
-      const { validatedAppProto, env } = await validateApp(data);
+      const { validatedAppProto } = await validateApp(data);
       setValidatedAppProto(validatedAppProto);
 
       if (source.type === "github") {
@@ -210,7 +213,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
           }
         );
 
-         const variables = env
+        const variables = env
           .filter((e) => !e.hidden && !e.deleted)
           .reduce((acc: Record<string, string>, item) => {
             acc[item.key] = item.value;
@@ -241,7 +244,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
         const addedEnvGroup = await z
           .object({
             env_group_name: z.string(),
-            env_group_version: z.number(),
+            env_group_version: z.coerce.bigint(),
           })
           .parseAsync(envGroupResponse.data);
         const envGroups = [
@@ -546,8 +549,9 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                             }
                           >
                             {detectedServices.count > 0
-                              ? `Detected ${detectedServices.count} service${detectedServices.count > 1 ? "s" : ""
-                              } from porter.yaml.`
+                              ? `Detected ${detectedServices.count} service${
+                                  detectedServices.count > 1 ? "s" : ""
+                                } from porter.yaml.`
                               : `Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.`}
                           </Text>
                         </AppearingDiv>
@@ -618,7 +622,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
           projectId={currentProject.id}
           clusterId={currentCluster.id}
           deployPorterApp={() =>
-            createAndApply({ app: validatedAppProto, source })
+            createAndApply({ app: validatedAppProto, source, env })
           }
           deploymentError={deployError}
           porterYamlPath={source.porter_yaml_path}

+ 29 - 7
internal/porter_app/environment.go

@@ -36,9 +36,11 @@ func WithEnvGroupFilter(envGroups []string) EnvVariableOption {
 
 // AppEnvironmentFromProtoInput is the input struct for AppEnvironmentFromProto
 type AppEnvironmentFromProtoInput struct {
-	App              *porterv1.PorterApp
-	DeploymentTarget *models.DeploymentTarget
-	K8SAgent         *kubernetes.Agent
+	ProjectID                  uint
+	ClusterID                  int
+	App                        *porterv1.PorterApp
+	K8SAgent                   *kubernetes.Agent
+	DeploymentTargetRepository repository.DeploymentTargetRepository
 }
 
 // AppEnvironmentFromProto returns all envfironment groups referenced in an app proto with their variables
@@ -48,8 +50,11 @@ func AppEnvironmentFromProto(ctx context.Context, inp AppEnvironmentFromProtoInp
 
 	envGroups := []environment_groups.EnvironmentGroup{}
 
-	if inp.DeploymentTarget == nil {
-		return nil, telemetry.Error(ctx, span, nil, "must provide a deployment target")
+	if inp.ProjectID == 0 {
+		return nil, telemetry.Error(ctx, span, nil, "must provide a project id")
+	}
+	if inp.ClusterID == 0 {
+		return nil, telemetry.Error(ctx, span, nil, "must provide a cluster id")
 	}
 	if inp.K8SAgent == nil {
 		return nil, telemetry.Error(ctx, span, nil, "must provide a kubernetes agent")
@@ -58,15 +63,32 @@ func AppEnvironmentFromProto(ctx context.Context, inp AppEnvironmentFromProtoInp
 		return nil, telemetry.Error(ctx, span, nil, "must provide an app")
 	}
 
+	deploymentTargets, err := inp.DeploymentTargetRepository.List(inp.ProjectID)
+	if err != nil {
+		return envGroups, telemetry.Error(ctx, span, err, "error reading deployment targets")
+	}
+
+	if len(deploymentTargets) == 0 {
+		return envGroups, telemetry.Error(ctx, span, nil, "no deployment targets found")
+	}
+	if len(deploymentTargets) > 1 {
+		return envGroups, telemetry.Error(ctx, span, nil, "more than one deployment target found")
+	}
+
+	deploymentTarget := deploymentTargets[0]
+	if deploymentTarget.ClusterID != inp.ClusterID {
+		return envGroups, telemetry.Error(ctx, span, nil, "deployment target does not belong to cluster")
+	}
+
 	var opts envVariarableOptions
 	for _, opt := range varOpts {
 		opt(&opts)
 	}
 
 	var namespace string
-	switch inp.DeploymentTarget.SelectorType {
+	switch deploymentTarget.SelectorType {
 	case models.DeploymentTargetSelectorType_Namespace:
-		namespace = inp.DeploymentTarget.Selector
+		namespace = deploymentTarget.Selector
 	default:
 		return envGroups, telemetry.Error(ctx, span, nil, "deployment target selector type not supported")
 	}

+ 76 - 0
internal/porter_app/revisions.go

@@ -10,6 +10,9 @@ import (
 	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/api-contracts/generated/go/porter/v1/porterv1connect"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/kubernetes/environment_groups"
+	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 
@@ -27,6 +30,8 @@ type Revision struct {
 	CreatedAt time.Time `json:"created_at"`
 	// UpdatedAt is the time the revision was updated
 	UpdatedAt time.Time `json:"updated_at"`
+	// Env is the environment variables for the revision
+	Env environment_groups.EnvironmentGroup `json:"env"`
 }
 
 // GetAppRevisionInput is the input struct for GetAppRevisions
@@ -108,3 +113,74 @@ func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevi
 
 	return revision, nil
 }
+
+// AttachEnvToRevisionInput is the input struct for AttachEnvToRevision
+type AttachEnvToRevisionInput struct {
+	ProjectID                  uint
+	ClusterID                  int
+	DeploymentTargetID         string
+	Revision                   Revision
+	K8SAgent                   *kubernetes.Agent
+	PorterAppRepository        repository.PorterAppRepository
+	DeploymentTargetRepository repository.DeploymentTargetRepository
+}
+
+// AttachEnvToRevision attaches the environment variables from the app's default env group to a revision
+// These are the variables that are displayed to the user in the UI as associated with the app rather than an env group
+func AttachEnvToRevision(ctx context.Context, inp AttachEnvToRevisionInput) (Revision, error) {
+	ctx, span := telemetry.NewSpan(ctx, "attach-env-to-revision")
+	defer span.End()
+
+	revision := inp.Revision
+
+	if inp.ProjectID == 0 {
+		return revision, telemetry.Error(ctx, span, nil, "must provide a project id")
+	}
+	if inp.ClusterID == 0 {
+		return revision, telemetry.Error(ctx, span, nil, "must provide a cluster id")
+	}
+	if inp.DeploymentTargetID == "" {
+		return revision, telemetry.Error(ctx, span, nil, "must provide a deployment target id")
+	}
+	if inp.K8SAgent == nil {
+		return revision, telemetry.Error(ctx, span, nil, "k8s agent is nil")
+	}
+
+	decoded, err := base64.StdEncoding.DecodeString(revision.B64AppProto)
+	if err != nil {
+		return revision, telemetry.Error(ctx, span, err, "error decoding app proto")
+	}
+
+	appDef := &porterv1.PorterApp{}
+	err = helpers.UnmarshalContractObject(decoded, appDef)
+	if err != nil {
+		return revision, telemetry.Error(ctx, span, err, "error unmarshalling app proto")
+	}
+
+	envName, err := AppEnvGroupName(ctx, appDef.Name, inp.DeploymentTargetID, uint(inp.ClusterID), inp.PorterAppRepository)
+	if err != nil {
+		return revision, telemetry.Error(ctx, span, err, "error getting app env group name")
+	}
+	envNameFilter := []string{envName}
+
+	envFromProtoInp := AppEnvironmentFromProtoInput{
+		ProjectID:                  inp.ProjectID,
+		ClusterID:                  inp.ClusterID,
+		App:                        appDef,
+		K8SAgent:                   inp.K8SAgent,
+		DeploymentTargetRepository: inp.DeploymentTargetRepository,
+	}
+	envGroups, err := AppEnvironmentFromProto(ctx, envFromProtoInp, WithEnvGroupFilter(envNameFilter), WithSecrets())
+	if err != nil {
+		return revision, telemetry.Error(ctx, span, err, "error getting app environment from revision")
+	}
+
+	if len(envGroups) > 1 {
+		return revision, telemetry.Error(ctx, span, err, "multiple app envs groups returned for same name")
+	}
+	if len(envGroups) == 1 {
+		revision.Env = envGroups[0]
+	}
+
+	return revision, nil
+}