Kaynağa Gözat

sync latest version of env groups when updating app (#3599)

ianedwards 2 yıl önce
ebeveyn
işleme
11eeb53405

+ 2 - 0
api/client/porter_app.go

@@ -442,6 +442,7 @@ func (c *Client) CreateOrUpdateAppEnvironment(
 	deploymentTargetID string,
 	variables map[string]string,
 	secrets map[string]string,
+	Base64AppProto string,
 ) (*porter_app.UpdateAppEnvironmentResponse, error) {
 	resp := &porter_app.UpdateAppEnvironmentResponse{}
 
@@ -450,6 +451,7 @@ func (c *Client) CreateOrUpdateAppEnvironment(
 		Variables:          variables,
 		Secrets:            secrets,
 		HardUpdate:         false,
+		Base64AppProto:     Base64AppProto,
 	}
 
 	err := c.postRequest(

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

@@ -115,10 +115,11 @@ func (c *GetBuildEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	envFromProtoInp := porter_app.AppEnvironmentFromProtoInput{
-		ProjectID: project.ID,
-		ClusterID: int(cluster.ID),
-		App:       appProto,
-		K8SAgent:  agent,
+		ProjectID:                  project.ID,
+		ClusterID:                  int(cluster.ID),
+		App:                        appProto,
+		K8SAgent:                   agent,
+		DeploymentTargetRepository: c.Repo().DeploymentTarget(),
 	}
 	envGroups, err := porter_app.AppEnvironmentFromProto(ctx, envFromProtoInp)
 	if err != nil {

+ 133 - 4
api/server/handlers/porter_app/update_app_environment_group.go

@@ -1,11 +1,14 @@
 package porter_app
 
 import (
+	"context"
+	"encoding/base64"
 	"net/http"
 	"strconv"
 	"strings"
 	"time"
 
+	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/porter_app"
 
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
@@ -13,6 +16,7 @@ import (
 
 	"connectrpc.com/connect"
 
+	"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"
@@ -54,6 +58,7 @@ const (
 
 // UpdateAppEnvironmentRequest represents the accepted fields on a request to the /apps/{porter_app_name}/environment-group endpoint
 type UpdateAppEnvironmentRequest struct {
+	Base64AppProto     string            `json:"b64_app_proto"`
 	DeploymentTargetID string            `json:"deployment_target_id"`
 	Variables          map[string]string `json:"variables"`
 	Secrets            map[string]string `json:"secrets"`
@@ -110,6 +115,27 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID})
 
+	if request.Base64AppProto == "" {
+		err := telemetry.Error(ctx, span, nil, "b64 yaml 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.StatusInternalServerError))
+		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.StatusInternalServerError))
+		return
+	}
+
 	deploymentTargetDetailsReq := connect.NewRequest(&porterv1.DeploymentTargetDetailsRequest{
 		ProjectId:          int64(project.ID),
 		DeploymentTargetId: request.DeploymentTargetID,
@@ -139,7 +165,7 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "hard-update", Value: request.HardUpdate})
 
-	envGroupName, err := porter_app.AppEnvGroupName(ctx, appName, request.DeploymentTargetID, cluster.ID, c.Repo().PorterApp())
+	appEnvGroupName, err := porter_app.AppEnvGroupName(ctx, appName, request.DeploymentTargetID, cluster.ID, c.Repo().PorterApp())
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error getting app env group name")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
@@ -153,7 +179,7 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		return
 	}
 
-	latestEnvironmentGroup, err := environment_groups.LatestBaseEnvironmentGroup(ctx, agent, envGroupName)
+	latestEnvironmentGroup, err := environment_groups.LatestBaseEnvironmentGroup(ctx, agent, appEnvGroupName)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "unable to get latest base environment group")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
@@ -190,6 +216,23 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "same-env-group", Value: sameEnvGroup})
 
 		if sameEnvGroup {
+			// even if the env group is the same, we still need to sync the latest versions of the other env groups
+			syncInp := syncLatestEnvGroupVersionsInput{
+				envGroups:          appProto.EnvGroups,
+				appName:            appName,
+				appEnvName:         appEnvGroupName,
+				sameAppEnv:         true,
+				namespace:          namespace,
+				deploymentTargetID: request.DeploymentTargetID,
+				k8sAgent:           agent,
+			}
+			err = syncLatestEnvGroupVersions(ctx, syncInp)
+			if err != nil {
+				err := telemetry.Error(ctx, span, err, "error syncing latest env group versions")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			}
+
 			res := &UpdateAppEnvironmentResponse{
 				EnvGroupName:    latestEnvironmentGroup.Name,
 				EnvGroupVersion: latestEnvironmentGroup.Version,
@@ -220,7 +263,7 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 
 	envGroup := environment_groups.EnvironmentGroup{
-		Name:            envGroupName,
+		Name:            appEnvGroupName,
 		Variables:       variables,
 		SecretVariables: secrets,
 		CreatedAtUTC:    time.Now().UTC(),
@@ -241,7 +284,7 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 
 	inp := environment_groups.SyncLatestVersionToNamespaceInput{
-		BaseEnvironmentGroupName: envGroupName,
+		BaseEnvironmentGroupName: appEnvGroupName,
 		TargetNamespace:          namespace,
 	}
 
@@ -267,6 +310,22 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		return
 	}
 
+	syncInp := syncLatestEnvGroupVersionsInput{
+		envGroups:          appProto.EnvGroups,
+		appName:            appName,
+		appEnvName:         appEnvGroupName,
+		sameAppEnv:         false,
+		namespace:          namespace,
+		deploymentTargetID: request.DeploymentTargetID,
+		k8sAgent:           agent,
+	}
+	err = syncLatestEnvGroupVersions(ctx, syncInp)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error syncing latest env group versions")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
 	res := &UpdateAppEnvironmentResponse{
 		EnvGroupName:    split[0],
 		EnvGroupVersion: version,
@@ -274,3 +333,73 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 
 	c.WriteResult(w, r, res)
 }
+
+type syncLatestEnvGroupVersionsInput struct {
+	// envGroups is the list of env groups to sync. We only need the names and will get the latest version of each from the porter-env-group ns
+	envGroups []*porterv1.EnvGroup
+	// appName is the name of the app
+	appName string
+	// appEnvName is the name of the app env. This is the env group created when the app is created for storing app-specific variables
+	appEnvName string
+	// sameAppEnv is true if the app env group variables are unchanged. If true, we do not need to sync the latest version of the app env group
+	sameAppEnv bool
+	// namespace is the namespace to sync the latest versions to
+	namespace string
+	// deploymentTargetID is the id of the deployment target
+	deploymentTargetID string
+	// k8sAgent is the kubernetes agent
+	k8sAgent *kubernetes.Agent
+}
+
+// syncLatestEnvGroupVersions syncs the latest versions of the env groups to the namespace where an app is deployed
+func syncLatestEnvGroupVersions(ctx context.Context, inp syncLatestEnvGroupVersionsInput) error {
+	ctx, span := telemetry.NewSpan(ctx, "sync-latest-env-group-versions")
+	defer span.End()
+
+	if inp.deploymentTargetID == "" {
+		return telemetry.Error(ctx, span, nil, "deployment target id is empty")
+	}
+	if inp.appName == "" {
+		return telemetry.Error(ctx, span, nil, "app name is empty")
+	}
+	if inp.appEnvName == "" {
+		return telemetry.Error(ctx, span, nil, "app env name is empty")
+	}
+	if inp.namespace == "" {
+		return telemetry.Error(ctx, span, nil, "namespace is empty")
+	}
+	if inp.k8sAgent == nil {
+		return telemetry.Error(ctx, span, nil, "k8s agent is nil")
+	}
+
+	for _, envGroup := range inp.envGroups {
+		if envGroup == nil {
+			continue
+		}
+
+		additionalEnvGroupLabels := map[string]string{
+			LabelKey_AppName:            inp.appName,
+			LabelKey_DeploymentTargetID: inp.deploymentTargetID,
+			LabelKey_PorterManaged:      "true",
+		}
+
+		if envGroup.GetName() == inp.appEnvName {
+			if inp.sameAppEnv {
+				continue
+			}
+
+			additionalEnvGroupLabels[environment_groups.LabelKey_DefaultAppEnvironment] = "true"
+		}
+
+		_, err := environment_groups.SyncLatestVersionToNamespace(ctx, inp.k8sAgent, environment_groups.SyncLatestVersionToNamespaceInput{
+			TargetNamespace:          inp.namespace,
+			BaseEnvironmentGroupName: envGroup.GetName(),
+		}, additionalEnvGroupLabels)
+		if err != nil {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "env-group-name", Value: envGroup.GetName()})
+			return telemetry.Error(ctx, span, err, "error syncing latest version to namespace")
+		}
+	}
+
+	return nil
+}

+ 1 - 1
cli/cmd/v2/apply.go

@@ -73,7 +73,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por
 			return fmt.Errorf("error getting app name from b64 app proto: %w", err)
 		}
 
-		envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, targetResp.DeploymentTargetID, parseResp.EnvVariables, parseResp.EnvSecrets)
+		envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, targetResp.DeploymentTargetID, parseResp.EnvVariables, parseResp.EnvSecrets, parseResp.B64AppProto)
 		if err != nil {
 			return fmt.Errorf("error calling create or update app environment group endpoint: %w", err)
 		}

+ 3 - 2
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -165,16 +165,17 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
       );
 
       // updates the default env group associated with this app to store app specific env vars
-      const res = await api.updateAppEnvironmentGroup(
+      const res = await api.updateEnvironmentGroupV2(
         "<token>",
         {
           deployment_target_id: deploymentTargetId,
           variables,
           secrets,
+          b64_app_proto: btoa(validatedAppProto.toJsonString()),
           remove_missing: true,
         },
         {
-          project_id: projectId,
+          id: projectId,
           cluster_id: clusterId,
           app_name: porterApp.name,
         }

+ 1 - 0
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -262,6 +262,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
           {
             deployment_target_id: deploymentTarget.deployment_target_id,
             variables: variables,
+            b64_app_proto: btoa(app.toJsonString()),
             secrets: secrets,
           },
           {

+ 1 - 17
dashboard/src/shared/api.tsx

@@ -914,22 +914,6 @@ const createApp = baseApi<
   return `/api/projects/${pathParams.project_id}/clusters/${pathParams.cluster_id}/apps/create`;
 });
 
-const updateAppEnvironmentGroup = baseApi<
-  {
-    deployment_target_id: string;
-    variables: Record<string, string>;
-    secrets: Record<string, string>;
-    remove_missing: boolean;
-  },
-  {
-    project_id: number;
-    cluster_id: number;
-    app_name: string;
-  }
->("POST", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/clusters/${pathParams.cluster_id}/apps/${pathParams.app_name}/update-environment`;
-});
-
 const applyApp = baseApi<
   {
     deployment_target_id: string;
@@ -1852,6 +1836,7 @@ const updateEnvironmentGroupV2 = baseApi<
     deployment_target_id: string;
     variables: Record<string, string>;
     secrets: Record<string, string>;
+    b64_app_proto: string;
     remove_missing?: boolean;
   },
   {
@@ -3108,7 +3093,6 @@ export default {
   getBranchHead,
   validatePorterApp,
   createApp,
-  updateAppEnvironmentGroup,
   applyApp,
   getAttachedEnvGroups,
   getLatestRevision,

+ 1 - 0
internal/porter_app/v2/yaml.go

@@ -100,6 +100,7 @@ type PorterYAML struct {
 	Env      map[string]string  `yaml:"env"`
 
 	Predeploy *Service `yaml:"predeploy"`
+	EnvGroups []string `yaml:"env_groups,omitempty"`
 }
 
 // Build represents the build settings for a Porter app