From 11eeb5340574f56d5a5abb000b0beeabde289674 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Wed, 20 Sep 2023 10:05:49 -0400 Subject: [PATCH] sync latest version of env groups when updating app (#3599) --- api/client/porter_app.go | 2 + .../handlers/porter_app/get_build_env.go | 9 +- .../update_app_environment_group.go | 137 +++++++++++++++++- cli/cmd/v2/apply.go | 2 +- .../app-view/AppDataContainer.tsx | 5 +- .../app-dashboard/create-app/CreateApp.tsx | 1 + dashboard/src/shared/api.tsx | 18 +-- internal/porter_app/v2/yaml.go | 1 + 8 files changed, 147 insertions(+), 28 deletions(-) diff --git a/api/client/porter_app.go b/api/client/porter_app.go index e5e7f879fb..081b8c55d8 100644 --- a/api/client/porter_app.go +++ b/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( diff --git a/api/server/handlers/porter_app/get_build_env.go b/api/server/handlers/porter_app/get_build_env.go index d60b82c802..6dec280c6e 100644 --- a/api/server/handlers/porter_app/get_build_env.go +++ b/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 { diff --git a/api/server/handlers/porter_app/update_app_environment_group.go b/api/server/handlers/porter_app/update_app_environment_group.go index 021a850713..22ed7b1655 100644 --- a/api/server/handlers/porter_app/update_app_environment_group.go +++ b/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 +} diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index 629a4cd789..1d74a596d0 100644 --- a/cli/cmd/v2/apply.go +++ b/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) } diff --git a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx index 045f5d83ad..1f7e28021a 100644 --- a/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx +++ b/dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx @@ -165,16 +165,17 @@ const AppDataContainer: React.FC = ({ 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( "", { 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, } diff --git a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx index 370d535333..96d7bc7408 100644 --- a/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx +++ b/dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx @@ -262,6 +262,7 @@ const CreateApp: React.FC = ({ history }) => { { deployment_target_id: deploymentTarget.deployment_target_id, variables: variables, + b64_app_proto: btoa(app.toJsonString()), secrets: secrets, }, { diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index 0b45043389..7ddbeae691 100644 --- a/dashboard/src/shared/api.tsx +++ b/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; - secrets: Record; - 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; secrets: Record; + b64_app_proto: string; remove_missing?: boolean; }, { @@ -3108,7 +3093,6 @@ export default { getBranchHead, validatePorterApp, createApp, - updateAppEnvironmentGroup, applyApp, getAttachedEnvGroups, getLatestRevision, diff --git a/internal/porter_app/v2/yaml.go b/internal/porter_app/v2/yaml.go index 57d73533fe..0cb2e0c044 100644 --- a/internal/porter_app/v2/yaml.go +++ b/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