diff --git a/api/server/handlers/porter_app/create_app_template.go b/api/server/handlers/porter_app/create_app_template.go index d6afedfd88..9bc9630ceb 100644 --- a/api/server/handlers/porter_app/create_app_template.go +++ b/api/server/handlers/porter_app/create_app_template.go @@ -2,21 +2,26 @@ package porter_app import ( "net/http" + "time" "github.com/google/uuid" + "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/server/shared/requestutils" "github.com/porter-dev/porter/api/types" + "github.com/porter-dev/porter/internal/kubernetes/environment_groups" "github.com/porter-dev/porter/internal/models" + "github.com/porter-dev/porter/internal/porter_app" "github.com/porter-dev/porter/internal/telemetry" ) // CreateAppTemplateHandler is the handler for the /app-template endpoint type CreateAppTemplateHandler struct { handlers.PorterHandlerReadWriter + authz.KubernetesAgentGetter } // NewCreateAppTemplateHandler handles POST requests to the endpoint /app-template @@ -27,12 +32,15 @@ func NewCreateAppTemplateHandler( ) *CreateAppTemplateHandler { return &CreateAppTemplateHandler{ PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer), + KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config), } } // CreateAppTemplateRequest is the request object for the /app-template POST endpoint type CreateAppTemplateRequest struct { - B64AppProto string `json:"b64_app_proto"` + B64AppProto string `json:"b64_app_proto"` + Variables map[string]string `json:"variables"` + Secrets map[string]string `json:"secrets"` } // CreateAppTemplateResponse is the response object for the /app-template POST endpoint @@ -46,6 +54,7 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ defer span.End() project, _ := ctx.Value(types.ProjectScope).(*models.Project) + cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster) if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled") @@ -74,6 +83,13 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ 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 + } + porterApps, err := c.Repo().PorterApp().ReadPorterAppsByProjectIDAndName(project.ID, appName) if err != nil { err := telemetry.Error(ctx, span, err, "error getting porter app from repo") @@ -142,6 +158,42 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ return } + previewTemplateEnvName, err := porter_app.AppTemplateEnvGroupName(ctx, appName, cluster.ID, c.Repo().PorterApp()) + if err != nil { + err := telemetry.Error(ctx, span, err, "unable to get app template env group name") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + envGroup, err := environment_groups.LatestBaseEnvironmentGroup(ctx, agent, previewTemplateEnvName) + 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)) + return + } + + if envGroup.Name == "" { + envGroup = environment_groups.EnvironmentGroup{ + Name: previewTemplateEnvName, + CreatedAtUTC: time.Now().UTC(), + } + } + envGroup.Variables = request.Variables + envGroup.SecretVariables = request.Secrets + + additionalEnvGroupLabels := map[string]string{ + LabelKey_AppName: appName, + environment_groups.LabelKey_DefaultAppEnvironment: "true", + LabelKey_PorterManaged: "true", + } + + err = environment_groups.CreateOrUpdateBaseEnvironmentGroup(ctx, agent, envGroup, additionalEnvGroupLabels) + if err != nil { + err := telemetry.Error(ctx, span, err, "unable to create or update base environment group") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + res := &CreateAppTemplateResponse{ AppTemplateID: updatedAppTemplate.ID.String(), } 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 1e695e9e61..d5629d7619 100644 --- a/api/server/handlers/porter_app/update_app_environment_group.go +++ b/api/server/handlers/porter_app/update_app_environment_group.go @@ -160,8 +160,10 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R } namespace := deploymentTargetDetailsResp.Msg.Namespace - telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "namespace", Value: namespace}) + isPreview := deploymentTargetDetailsResp.Msg.IsPreview + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "is-preview", Value: isPreview}) + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "namespace", Value: namespace}) telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "hard-update", Value: request.HardUpdate}) appEnvGroupName, err := porter_app.AppEnvGroupName(ctx, appName, request.DeploymentTargetID, cluster.ID, c.Repo().PorterApp()) @@ -187,6 +189,21 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "env-group-exists", Value: latestEnvironmentGroup.Name != ""}) + previewTemplateEnvName, err := porter_app.AppTemplateEnvGroupName(ctx, appName, cluster.ID, c.Repo().PorterApp()) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting preview template env name") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + // filter out preview template and app env groups + filteredEnvGroups := []*porterv1.EnvGroup{} + for _, envGroup := range appProto.EnvGroups { + if envGroup.GetName() != previewTemplateEnvName && envGroup.GetName() != appEnvGroupName { + filteredEnvGroups = append(filteredEnvGroups, envGroup) + } + } + if latestEnvironmentGroup.Name != "" { sameEnvGroup := true for key, newValue := range request.Variables { @@ -217,9 +234,8 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R 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, + envGroups: filteredEnvGroups, appName: appName, - appEnvName: appEnvGroupName, namespace: namespace, deploymentTargetID: request.DeploymentTargetID, k8sAgent: agent, @@ -245,6 +261,18 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R } } + // if this app does not have a default env group for this deployment target and is a preview + // then use the preview template env group as the default + // this should only run when the app is first deployed to a given deployment target + if latestEnvironmentGroup.Name == "" && isPreview { + latestEnvironmentGroup, err = environment_groups.LatestBaseEnvironmentGroup(ctx, agent, previewTemplateEnvName) + 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)) + return + } + } + variables := make(map[string]string) secrets := make(map[string]string) @@ -299,9 +327,8 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "env-group-versioned-name", Value: syncedAppEnvironment.EnvironmentGroupVersionedName}) syncInp := syncLatestEnvGroupVersionsInput{ - envGroups: appProto.EnvGroups, + envGroups: filteredEnvGroups, appName: appName, - appEnvName: appEnvGroupName, namespace: namespace, deploymentTargetID: request.DeploymentTargetID, k8sAgent: agent, @@ -344,8 +371,6 @@ type syncLatestEnvGroupVersionsInput struct { 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 // namespace is the namespace to sync the latest versions to namespace string // deploymentTargetID is the id of the deployment target @@ -367,9 +392,6 @@ func syncLatestEnvGroupVersions(ctx context.Context, inp syncLatestEnvGroupVersi if inp.appName == "" { return envGroups, telemetry.Error(ctx, span, nil, "app name is empty") } - if inp.appEnvName == "" { - return envGroups, telemetry.Error(ctx, span, nil, "app env name is empty") - } if inp.namespace == "" { return envGroups, telemetry.Error(ctx, span, nil, "namespace is empty") } @@ -381,9 +403,6 @@ func syncLatestEnvGroupVersions(ctx context.Context, inp syncLatestEnvGroupVersi if envGroup == nil { continue } - if envGroup.GetName() == inp.appEnvName { - continue - } additionalEnvGroupLabels := map[string]string{ LabelKey_AppName: inp.appName, diff --git a/go.mod b/go.mod index b6e43cb59b..eb077c9248 100644 --- a/go.mod +++ b/go.mod @@ -82,7 +82,7 @@ require ( github.com/matryer/is v1.4.0 github.com/nats-io/nats.go v1.24.0 github.com/open-policy-agent/opa v0.44.0 - github.com/porter-dev/api-contracts v0.2.6 + github.com/porter-dev/api-contracts v0.2.8 github.com/riandyrn/otelchi v0.5.1 github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d diff --git a/go.sum b/go.sum index b5ea2a2421..f7cad813c8 100644 --- a/go.sum +++ b/go.sum @@ -1516,8 +1516,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= -github.com/porter-dev/api-contracts v0.2.6 h1:5Z/Qr1Qv6iAM4rCUfpa9+HougO8K2HFjGOeSLDZFfDw= -github.com/porter-dev/api-contracts v0.2.6/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= +github.com/porter-dev/api-contracts v0.2.8 h1:z5BZihdZ75J5Dz3jCeX6ziE/wt6h4yhFqaYoO3BqBY8= +github.com/porter-dev/api-contracts v0.2.8/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M= github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= diff --git a/internal/porter_app/environment.go b/internal/porter_app/environment.go index 8d5cb8fbea..da39c18648 100644 --- a/internal/porter_app/environment.go +++ b/internal/porter_app/environment.go @@ -144,3 +144,27 @@ func AppEnvGroupName(ctx context.Context, appName string, deploymentTargetId str return fmt.Sprintf("%d-%s", porterApp.ID, deploymentTargetId[:6]), nil } + +// AppTemplateEnvGroupName returns the name of the environment group for an app template +func AppTemplateEnvGroupName(ctx context.Context, appName string, clusterID uint, porterAppRepository repository.PorterAppRepository) (string, error) { + ctx, span := telemetry.NewSpan(ctx, "app-template-env-group-name") + defer span.End() + + if appName == "" { + return "", telemetry.Error(ctx, span, nil, "app name is empty") + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName}) + + if clusterID == 0 { + return "", telemetry.Error(ctx, span, nil, "cluster id is empty") + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cluster-id", Value: clusterID}) + + porterApp, err := porterAppRepository.ReadPorterAppByName(clusterID, appName) + if err != nil { + return "", telemetry.Error(ctx, span, err, "error reading porter app by name") + } + telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-id", Value: porterApp.ID}) + + return fmt.Sprintf("%d-template-preview", porterApp.ID), nil +}