Răsfoiți Sursa

Merge branch 'master' of github.com:porter-dev/porter into status-tab

Feroze Mohideen 2 ani în urmă
părinte
comite
e82a99550b
40 a modificat fișierele cu 2367 adăugiri și 400 ștergeri
  1. 25 0
      api/client/env_groups.go
  2. 30 4
      api/client/porter_app.go
  3. 88 0
      api/server/handlers/environment_groups/latest_variables.go
  4. 119 0
      api/server/handlers/porter_app/app_env_variables.go
  5. 63 121
      api/server/handlers/porter_app/create_app_template.go
  6. 15 13
      api/server/handlers/porter_app/pod_status.go
  7. 19 8
      api/server/handlers/porter_app/update_image.go
  8. 29 0
      api/server/router/cluster.go
  9. 29 0
      api/server/router/porter_app.go
  10. 1 0
      cli/cmd/commands/all.go
  11. 15 17
      cli/cmd/commands/app.go
  12. 165 0
      cli/cmd/commands/env.go
  13. 4 9
      cli/cmd/v2/update_image.go
  14. 82 7
      dashboard/package-lock.json
  15. 2 1
      dashboard/package.json
  16. 0 0
      dashboard/src/assets/postgresql.svg
  17. 4 1
      dashboard/src/components/CloudFormationForm.tsx
  18. 65 0
      dashboard/src/lib/addons/index.ts
  19. 28 0
      dashboard/src/lib/addons/postgres.ts
  20. 61 0
      dashboard/src/lib/hooks/useAppWithPreviewOverrides.ts
  21. 21 7
      dashboard/src/lib/porter-apps/index.ts
  22. 20 18
      dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx
  23. 31 39
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx
  24. 37 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/Addons.tsx
  25. 125 136
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx
  26. 203 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/RequiredApps.tsx
  27. 72 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/ServiceSettings.tsx
  28. 2 2
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/SetupApp.tsx
  29. 9 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/types.ts
  30. 175 0
      dashboard/src/main/home/managed-addons/AddonListRow.tsx
  31. 246 0
      dashboard/src/main/home/managed-addons/AddonsList.tsx
  32. 191 0
      dashboard/src/main/home/managed-addons/tabs/PostgresTabs.tsx
  33. 5 0
      dashboard/src/shared/api.tsx
  34. 1 0
      dashboard/src/shared/types.tsx
  35. 7 2
      dashboard/webpack.config.js
  36. 1 1
      go.mod
  37. 2 2
      go.sum
  38. 357 0
      go.work.sum
  39. 11 10
      internal/deployment_target/get.go
  40. 7 2
      zarf/helm/.dashboardenv

+ 25 - 0
api/client/env_groups.go

@@ -0,0 +1,25 @@
+package client
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/porter-dev/porter/api/server/handlers/environment_groups"
+)
+
+// GetLatestEnvGroupVariables gets the latest environment group variables for a given environment group
+func (c *Client) GetLatestEnvGroupVariables(
+	ctx context.Context,
+	projID, clusterID uint,
+	envGroupName string,
+) (*environment_groups.LatestEnvGroupVariablesResponse, error) {
+	resp := &environment_groups.LatestEnvGroupVariablesResponse{}
+
+	err := c.getRequest(
+		fmt.Sprintf("/projects/%d/clusters/%d/environment-groups/%s/latest", projID, clusterID, envGroupName),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}

+ 30 - 4
api/client/porter_app.go

@@ -543,6 +543,28 @@ func (c *Client) GetBuildEnv(
 	return resp, err
 	return resp, err
 }
 }
 
 
+// GetAppEnvVariables returns all env variables for a given app
+func (c *Client) GetAppEnvVariables(
+	ctx context.Context,
+	projectID uint, clusterID uint,
+	appName string,
+) (*porter_app.AppEnvVariablesResponse, error) {
+	resp := &porter_app.AppEnvVariablesResponse{}
+
+	req := &porter_app.AppEnvVariablesRequest{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/apps/%s/env-variables",
+			projectID, clusterID, appName,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
 // GetBuildFromRevision returns the build environment for a given app proto
 // GetBuildFromRevision returns the build environment for a given app proto
 func (c *Client) GetBuildFromRevision(
 func (c *Client) GetBuildFromRevision(
 	ctx context.Context,
 	ctx context.Context,
@@ -634,8 +656,12 @@ func (c *Client) PorterYamlV2Pods(
 	ctx context.Context,
 	ctx context.Context,
 	projectID, clusterID uint,
 	projectID, clusterID uint,
 	porterAppName string,
 	porterAppName string,
-	req *types.PorterYamlV2PodsRequest,
+	deploymentTargetName string,
 ) (*types.GetReleaseAllPodsResponse, error) {
 ) (*types.GetReleaseAllPodsResponse, error) {
+	req := &porter_app.PodStatusRequest{
+		DeploymentTargetName: deploymentTargetName,
+	}
+
 	resp := &types.GetReleaseAllPodsResponse{}
 	resp := &types.GetReleaseAllPodsResponse{}
 
 
 	err := c.getRequest(
 	err := c.getRequest(
@@ -655,11 +681,11 @@ func (c *Client) PorterYamlV2Pods(
 func (c *Client) UpdateImage(
 func (c *Client) UpdateImage(
 	ctx context.Context,
 	ctx context.Context,
 	projectID, clusterID uint,
 	projectID, clusterID uint,
-	appName, deploymentTargetId, tag string,
+	appName, deploymentTargetName, tag string,
 ) (*porter_app.UpdateImageResponse, error) {
 ) (*porter_app.UpdateImageResponse, error) {
 	req := &porter_app.UpdateImageRequest{
 	req := &porter_app.UpdateImageRequest{
-		Tag:                tag,
-		DeploymentTargetId: deploymentTargetId,
+		Tag:                  tag,
+		DeploymentTargetName: deploymentTargetName,
 	}
 	}
 
 
 	resp := &porter_app.UpdateImageResponse{}
 	resp := &porter_app.UpdateImageResponse{}

+ 88 - 0
api/server/handlers/environment_groups/latest_variables.go

@@ -0,0 +1,88 @@
+package environment_groups
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	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"
+)
+
+// LatestEnvGroupVariablesHandler is the handler for the /projects/{project_id}/clusters/{cluster_id}/environment-groups/{env_group_name}/latest endpoint
+type LatestEnvGroupVariablesHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewLatestEnvGroupVariablesHandler handles GET requests to /projects/{project_id}/clusters/{cluster_id}/environment-groups/{env_group_name}/latest
+func NewLatestEnvGroupVariablesHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *LatestEnvGroupVariablesHandler {
+	return &LatestEnvGroupVariablesHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// LatestEnvGroupVariablesRequest is the request object for the /projects/{project_id}/clusters/{cluster_id}/environment-groups/{env_group_name}/latest endpoint
+type LatestEnvGroupVariablesRequest struct{}
+
+// LatestEnvGroupVariablesResponse is the response object for the /projects/{project_id}/clusters/{cluster_id}/environment-groups/{env_group_name}/latest endpoint
+type LatestEnvGroupVariablesResponse struct {
+	Variables map[string]string `json:"variables"`
+	Secrets   map[string]string `json:"secrets"`
+}
+
+// ServeHTTP retrieves the latest env group variables from CCP and writes them to the response
+func (c *LatestEnvGroupVariablesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-latest-env-group-variables")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	envGroupName, reqErr := requestutils.GetURLParamString(r, types.URLParamEnvGroupName)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, reqErr, "error parsing env group name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	latestVariablesReq := connect.NewRequest(&porterv1.LatestEnvGroupWithVariablesRequest{
+		ProjectId:    int64(project.ID),
+		ClusterId:    int64(cluster.ID),
+		EnvGroupName: envGroupName,
+	})
+
+	ccpResp, err := c.Config().ClusterControlPlaneClient.LatestEnvGroupWithVariables(ctx, latestVariablesReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting env group variables from ccp")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if ccpResp == nil || ccpResp.Msg == nil {
+		err := telemetry.Error(ctx, span, nil, "ccp returned nil response")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+	if ccpResp.Msg.EnvGroupVariables == nil {
+		err := telemetry.Error(ctx, span, nil, "no env variables returned")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := &LatestEnvGroupVariablesResponse{
+		Variables: ccpResp.Msg.EnvGroupVariables.Normal,
+		Secrets:   ccpResp.Msg.EnvGroupVariables.Secret,
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 119 - 0
api/server/handlers/porter_app/app_env_variables.go

@@ -0,0 +1,119 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"connectrpc.com/connect"
+	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"
+)
+
+// AppEnvVariablesHandler is the handler for the /apps/{porter_app_name}/env-variables endpoint
+type AppEnvVariablesHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewAppEnvVariablesHandler handles GET requests to /apps/{porter_app_name}/env-variables
+func NewAppEnvVariablesHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *AppEnvVariablesHandler {
+	return &AppEnvVariablesHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// EnvVariables is a struct containing maps of the app's env variables and secrets
+type EnvVariables struct {
+	Variables map[string]string `json:"variables"`
+	Secrets   map[string]string `json:"secrets"`
+}
+
+// AppEnvVariablesRequest is the request object for the /apps/{porter_app_name}/env-variables endpoint
+type AppEnvVariablesRequest struct {
+	DeploymentTargetID string `schema:"deployment_target_id"`
+}
+
+// AppEnvVariablesResponse is the response object for the /apps/{porter_app_name}/env-variables endpoint
+type AppEnvVariablesResponse struct {
+	EnvVariables EnvVariables `json:"env_variables"`
+}
+
+// ServeHTTP retrieves the app's env variables from CCP and writes them to the response
+func (c *AppEnvVariablesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-app-env-variables")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
+	if reqErr != nil {
+		e := telemetry.Error(ctx, span, reqErr, "error parsing stack name from url")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName})
+
+	request := &LatestAppRevisionRequest{}
+	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
+	}
+
+	// optional deployment target id - if not provided, use the cluster's default
+	deploymentTargetID := request.DeploymentTargetID
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: deploymentTargetID})
+
+	var deploymentTargetIdentifer *porterv1.DeploymentTargetIdentifier
+	if deploymentTargetID != "" {
+		deploymentTargetIdentifer = &porterv1.DeploymentTargetIdentifier{
+			Id: deploymentTargetID,
+		}
+	}
+
+	appEnvVariablesReq := connect.NewRequest(&porterv1.AppEnvVariablesRequest{
+		ProjectId:                  int64(project.ID),
+		ClusterId:                  int64(cluster.ID),
+		AppName:                    appName,
+		DeploymentTargetIdentifier: deploymentTargetIdentifer,
+	})
+
+	ccpResp, err := c.Config().ClusterControlPlaneClient.AppEnvVariables(ctx, appEnvVariablesReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting app env variables from ccp")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if ccpResp == nil || ccpResp.Msg == nil {
+		err := telemetry.Error(ctx, span, nil, "ccp returned nil response")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+	if ccpResp.Msg.EnvVariables == nil {
+		err := telemetry.Error(ctx, span, nil, "no env variables returned")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	envVars := EnvVariables{
+		Variables: ccpResp.Msg.EnvVariables.Normal,
+		Secrets:   ccpResp.Msg.EnvVariables.Secret,
+	}
+
+	// write the app to the response
+	c.WriteResult(w, r, &AppEnvVariablesResponse{
+		EnvVariables: envVars,
+	})
+}

+ 63 - 121
api/server/handlers/porter_app/create_app_template.go

@@ -4,8 +4,8 @@ import (
 	"context"
 	"context"
 	"encoding/base64"
 	"encoding/base64"
 	"net/http"
 	"net/http"
-	"time"
 
 
+	"connectrpc.com/connect"
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
@@ -41,18 +41,25 @@ func NewCreateAppTemplateHandler(
 	}
 	}
 }
 }
 
 
+// Base64AddonWithEnvVars is a struct that contains a base64 encoded addon proto and its env vars
+// These env vars will be used to create an env group that is attached to the addon
+type Base64AddonWithEnvVars struct {
+	Base64Addon string            `json:"base64_addon"`
+	Variables   map[string]string `json:"variables"`
+	Secrets     map[string]string `json:"secrets"`
+}
+
 // CreateAppTemplateRequest is the request object for the /app-template POST endpoint
 // CreateAppTemplateRequest is the request object for the /app-template POST endpoint
 type CreateAppTemplateRequest struct {
 type CreateAppTemplateRequest struct {
-	B64AppProto            string            `json:"b64_app_proto"`
-	Variables              map[string]string `json:"variables"`
-	Secrets                map[string]string `json:"secrets"`
-	BaseDeploymentTargetID string            `json:"base_deployment_target_id"`
+	B64AppProto            string                   `json:"b64_app_proto"`
+	Variables              map[string]string        `json:"variables"`
+	Secrets                map[string]string        `json:"secrets"`
+	BaseDeploymentTargetID string                   `json:"base_deployment_target_id"`
+	Addons                 []Base64AddonWithEnvVars `json:"addons"`
 }
 }
 
 
 // CreateAppTemplateResponse is the response object for the /app-template POST endpoint
 // CreateAppTemplateResponse is the response object for the /app-template POST endpoint
-type CreateAppTemplateResponse struct {
-	AppTemplateID string `json:"app_template_id"`
-}
+type CreateAppTemplateResponse struct{}
 
 
 // ServeHTTP creates or updates an app template for a given porter app
 // ServeHTTP creates or updates an app template for a given porter app
 func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -108,55 +115,6 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 		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")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-	if len(porterApps) == 0 {
-		err := telemetry.Error(ctx, span, err, "no porter apps returned")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-	if len(porterApps) > 1 {
-		err := telemetry.Error(ctx, span, err, "multiple porter apps returned; unable to determine which one to use")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-
-	if porterApps[0].ID == 0 {
-		err := telemetry.Error(ctx, span, err, "porter app id is missing")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-id", Value: porterApps[0].ID})
-
-	var appTemplate *models.AppTemplate
-
-	existingAppTemplate, err := c.Repo().AppTemplate().AppTemplateByPorterAppID(
-		project.ID,
-		porterApps[0].ID,
-	)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error checking for existing app template")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-
-	if existingAppTemplate.ID != uuid.Nil {
-		appTemplate = existingAppTemplate
-		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "update-app-template", Value: true})
-	}
-	if appTemplate == nil {
-		appTemplate = &models.AppTemplate{
-			ProjectID:   int(project.ID),
-			PorterAppID: int(porterApps[0].ID),
-		}
-		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "update-app-template", Value: false})
-	}
-
 	protoWithoutDefaultAppEnvGroups, err := filterDefaultAppEnvGroups(ctx, request.B64AppProto, agent)
 	protoWithoutDefaultAppEnvGroups, err := filterDefaultAppEnvGroups(ctx, request.B64AppProto, agent)
 	if err != nil {
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error filtering default app env groups")
 		err := telemetry.Error(ctx, span, err, "error filtering default app env groups")
@@ -164,59 +122,53 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 		return
 	}
 	}
 
 
-	appTemplate.Base64App = protoWithoutDefaultAppEnvGroups
-	appTemplate.BaseDeploymentTargetID = baseDeploymentTarget
-
-	updatedAppTemplate, err := c.Repo().AppTemplate().CreateAppTemplate(appTemplate)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error creating app template")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
+	var addonTemplates []*porterv1.AddonWithEnvVars
+	for _, addon := range request.Addons {
+		decoded, err := base64.StdEncoding.DecodeString(addon.Base64Addon)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error decoding base64 addon")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
 
 
-	if updatedAppTemplate == nil {
-		err := telemetry.Error(ctx, span, err, "updated app template is nil")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-	if updatedAppTemplate.ID == uuid.Nil {
-		err := telemetry.Error(ctx, span, err, "updated app template id is nil")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
+		addonProto := &porterv1.Addon{}
+		err = helpers.UnmarshalContractObject(decoded, addonProto)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error unmarshalling addon proto")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			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
-	}
+		addonTemplates = append(addonTemplates, &porterv1.AddonWithEnvVars{
+			Addon: addonProto,
+			EnvVars: &porterv1.EnvGroupVariables{
+				Normal: addon.Variables,
+				Secret: addon.Secrets,
+			},
+		})
+	}
+
+	updateAppTemplateReq := connect.NewRequest(&porterv1.UpdateAppTemplateRequest{
+		ProjectId:   int64(project.ID),
+		AppName:     appName,
+		AppTemplate: protoWithoutDefaultAppEnvGroups,
+		AppEnv: &porterv1.EnvGroupVariables{
+			Normal: request.Variables,
+			Secret: request.Secrets,
+		},
+		AddonTemplates:         addonTemplates,
+		BaseDeploymentTargetId: baseDeploymentTarget.String(),
+	})
 
 
-	envGroup, err := environment_groups.LatestBaseEnvironmentGroup(ctx, agent, previewTemplateEnvName)
+	updateAppTemplateRes, err := c.Config().ClusterControlPlaneClient.UpdateAppTemplate(ctx, updateAppTemplateReq)
 	if err != nil {
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "unable to get latest base environment group")
+		err := telemetry.Error(ctx, span, err, "error updating app template")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 		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")
+	if updateAppTemplateRes == nil || updateAppTemplateRes.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "error updating app template")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 		return
 	}
 	}
@@ -238,9 +190,7 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 		return
 	}
 	}
 
 
-	res := &CreateAppTemplateResponse{
-		AppTemplateID: updatedAppTemplate.ID.String(),
-	}
+	res := &CreateAppTemplateResponse{}
 
 
 	c.WriteResult(w, r, res)
 	c.WriteResult(w, r, res)
 }
 }
@@ -248,35 +198,34 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 // filterDefaultAppEnvGroups filters out any default app env groups found when creating an app template
 // filterDefaultAppEnvGroups filters out any default app env groups found when creating an app template
 // app templates are based on the latest version of a given app, so it is possible for this env group to be included
 // app templates are based on the latest version of a given app, so it is possible for this env group to be included
 // however, the app template will get its own default env group when used to deploy to a preview environment
 // however, the app template will get its own default env group when used to deploy to a preview environment
-func filterDefaultAppEnvGroups(ctx context.Context, b64AppProto string, agent *kubernetes.Agent) (string, error) {
+func filterDefaultAppEnvGroups(ctx context.Context, b64AppProto string, agent *kubernetes.Agent) (*porterv1.PorterApp, error) {
 	ctx, span := telemetry.NewSpan(ctx, "filter-default-app-env-groups")
 	ctx, span := telemetry.NewSpan(ctx, "filter-default-app-env-groups")
 	defer span.End()
 	defer span.End()
 
 
-	var finalAppProto string
+	appProto := &porterv1.PorterApp{}
 
 
 	if b64AppProto == "" {
 	if b64AppProto == "" {
-		return finalAppProto, telemetry.Error(ctx, span, nil, "b64 app proto is empty")
+		return appProto, telemetry.Error(ctx, span, nil, "b64 app proto is empty")
 	}
 	}
 	if agent == nil {
 	if agent == nil {
-		return finalAppProto, telemetry.Error(ctx, span, nil, "agent is nil")
+		return appProto, telemetry.Error(ctx, span, nil, "agent is nil")
 	}
 	}
 
 
 	decoded, err := base64.StdEncoding.DecodeString(b64AppProto)
 	decoded, err := base64.StdEncoding.DecodeString(b64AppProto)
 	if err != nil {
 	if err != nil {
-		return finalAppProto, telemetry.Error(ctx, span, err, "error decoding base app")
+		return appProto, telemetry.Error(ctx, span, err, "error decoding base app")
 	}
 	}
 
 
-	appProto := &porterv1.PorterApp{}
 	err = helpers.UnmarshalContractObject(decoded, appProto)
 	err = helpers.UnmarshalContractObject(decoded, appProto)
 	if err != nil {
 	if err != nil {
-		return finalAppProto, telemetry.Error(ctx, span, err, "error unmarshalling app proto")
+		return appProto, telemetry.Error(ctx, span, err, "error unmarshalling app proto")
 	}
 	}
 
 
 	filteredEnvGroups := []*porterv1.EnvGroup{}
 	filteredEnvGroups := []*porterv1.EnvGroup{}
 	for _, envGroup := range appProto.EnvGroups {
 	for _, envGroup := range appProto.EnvGroups {
 		baseEnvGroup, err := environment_groups.LatestBaseEnvironmentGroup(ctx, agent, envGroup.Name)
 		baseEnvGroup, err := environment_groups.LatestBaseEnvironmentGroup(ctx, agent, envGroup.Name)
 		if err != nil {
 		if err != nil {
-			return finalAppProto, telemetry.Error(ctx, span, err, "unable to get latest base environment group")
+			return appProto, telemetry.Error(ctx, span, err, "unable to get latest base environment group")
 		}
 		}
 		if baseEnvGroup.DefaultAppEnvironment {
 		if baseEnvGroup.DefaultAppEnvironment {
 			continue
 			continue
@@ -287,12 +236,5 @@ func filterDefaultAppEnvGroups(ctx context.Context, b64AppProto string, agent *k
 
 
 	appProto.EnvGroups = filteredEnvGroups
 	appProto.EnvGroups = filteredEnvGroups
 
 
-	encoded, err := helpers.MarshalContractObject(ctx, appProto)
-	if err != nil {
-		return finalAppProto, telemetry.Error(ctx, span, err, "error marshalling app proto")
-	}
-
-	finalAppProto = base64.StdEncoding.EncodeToString(encoded)
-
-	return finalAppProto, nil
+	return appProto, nil
 }
 }

+ 15 - 13
api/server/handlers/porter_app/pod_status.go

@@ -68,20 +68,19 @@ func (c *ServiceStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
 
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "service-name", Value: request.ServiceName}, telemetry.AttributeKV{Key: "app-name", Value: appName})
-
-	if request.DeploymentTargetID == "" {
-		err := telemetry.Error(ctx, span, nil, "must provide deployment target id")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID})
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "service-name", Value: request.ServiceName},
+		telemetry.AttributeKV{Key: "app-name", Value: appName},
+		telemetry.AttributeKV{Key: "input-deployment-target-id", Value: request.DeploymentTargetID},
+		telemetry.AttributeKV{Key: "input-deployment-target-name", Value: request.DeploymentTargetName},
+	)
 
 
 	deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{
 	deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{
-		ProjectID:          int64(project.ID),
-		ClusterID:          int64(cluster.ID),
-		DeploymentTargetID: request.DeploymentTargetID,
-		CCPClient:          c.Config().ClusterControlPlaneClient,
+		ProjectID:            int64(project.ID),
+		ClusterID:            int64(cluster.ID),
+		DeploymentTargetID:   request.DeploymentTargetID,
+		DeploymentTargetName: request.DeploymentTargetName,
+		CCPClient:            c.Config().ClusterControlPlaneClient,
 	})
 	})
 	if err != nil {
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error getting deployment target details")
 		err := telemetry.Error(ctx, span, err, "error getting deployment target details")
@@ -90,7 +89,10 @@ func (c *ServiceStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	}
 	}
 
 
 	namespace := deploymentTarget.Namespace
 	namespace := deploymentTarget.Namespace
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "namespace", Value: namespace})
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "namespace", Value: namespace},
+		telemetry.AttributeKV{Key: "deployment-target-id", Value: deploymentTarget.ID},
+	)
 
 
 	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	if err != nil {
 	if err != nil {

+ 19 - 8
api/server/handlers/porter_app/update_image.go

@@ -36,9 +36,10 @@ func NewUpdateImageHandler(
 
 
 // UpdateImageRequest is the request object for the /apps/{porter_app_name}/update-image endpoint
 // UpdateImageRequest is the request object for the /apps/{porter_app_name}/update-image endpoint
 type UpdateImageRequest struct {
 type UpdateImageRequest struct {
-	DeploymentTargetId string `json:"deployment_target_id"`
-	Repository         string `json:"repository"`
-	Tag                string `json:"tag"`
+	DeploymentTargetId   string `json:"deployment_target_id"`
+	DeploymentTargetName string `json:"deployment_target_name"`
+	Repository           string `json:"repository"`
+	Tag                  string `json:"tag"`
 }
 }
 
 
 // UpdateImageResponse is the response object for the /apps/{porter_app_name}/update-image endpoint
 // UpdateImageResponse is the response object for the /apps/{porter_app_name}/update-image endpoint
@@ -74,12 +75,22 @@ func (c *UpdateImageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetId},
+		telemetry.AttributeKV{Key: "deployment-target-name", Value: request.DeploymentTargetName},
+		telemetry.AttributeKV{Key: "repository", Value: request.Repository},
+		telemetry.AttributeKV{Key: "tag", Value: request.Tag},
+	)
+
 	updateImageReq := connect.NewRequest(&porterv1.UpdateAppImageRequest{
 	updateImageReq := connect.NewRequest(&porterv1.UpdateAppImageRequest{
-		ProjectId:          int64(project.ID),
-		DeploymentTargetId: request.DeploymentTargetId,
-		RepositoryUrl:      request.Repository,
-		Tag:                request.Tag,
-		AppName:            appName,
+		ProjectId:     int64(project.ID),
+		RepositoryUrl: request.Repository,
+		Tag:           request.Tag,
+		AppName:       appName,
+		DeploymentTargetIdentifier: &porterv1.DeploymentTargetIdentifier{
+			Id:   request.DeploymentTargetId,
+			Name: request.DeploymentTargetName,
+		},
 	})
 	})
 	ccpResp, err := c.Config().ClusterControlPlaneClient.UpdateAppImage(ctx, updateImageReq)
 	ccpResp, err := c.Config().ClusterControlPlaneClient.UpdateAppImage(ctx, updateImageReq)
 	if err != nil {
 	if err != nil {

+ 29 - 0
api/server/router/cluster.go

@@ -1722,6 +1722,35 @@ func getClusterRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/environment-groups/{env_group_name}/latest
+	getLatestEnvironmentGroupVariablesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/environment-groups/{%s}/latest", relPath, types.URLParamEnvGroupName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getLatestEnvironmentGroupVariablesHandler := environment_groups.NewLatestEnvGroupVariablesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getLatestEnvironmentGroupVariablesEndpoint,
+		Handler:  getLatestEnvironmentGroupVariablesHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/environment-groups/update-linked-apps
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/environment-groups/update-linked-apps
 	updateLinkedAppsEndpoint := factory.NewAPIEndpoint(
 	updateLinkedAppsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{

+ 29 - 0
api/server/router/porter_app.go

@@ -601,6 +601,35 @@ func getPorterAppRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/env-variables -> porter_app.AppEnvVariablesHandler
+	appEnvVariablesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/env-variables", relPathV2, types.URLParamPorterAppName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	appEnvVariablesHandler := porter_app.NewAppEnvVariablesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: appEnvVariablesEndpoint,
+		Handler:  appEnvVariablesHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id}/yaml -> porter_app.NewPorterYAMLFromRevisionHandler
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id}/yaml -> porter_app.NewPorterYAMLFromRevisionHandler
 	porterYAMLFromRevision := factory.NewAPIEndpoint(
 	porterYAMLFromRevision := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{

+ 1 - 0
cli/cmd/commands/all.go

@@ -48,6 +48,7 @@ func RegisterCommands() (*cobra.Command, error) {
 	rootCmd.AddCommand(registerCommand_Stack(cliConf))
 	rootCmd.AddCommand(registerCommand_Stack(cliConf))
 	rootCmd.AddCommand(registerCommand_Update(cliConf))
 	rootCmd.AddCommand(registerCommand_Update(cliConf))
 	rootCmd.AddCommand(registerCommand_Version(cliConf))
 	rootCmd.AddCommand(registerCommand_Version(cliConf))
+	rootCmd.AddCommand(registerCommand_Env(cliConf))
 	return rootCmd, nil
 	return rootCmd, nil
 }
 }
 
 

+ 15 - 17
cli/cmd/commands/app.go

@@ -40,6 +40,7 @@ var (
 	appInteractive   bool
 	appInteractive   bool
 	appContainerName string
 	appContainerName string
 	appTag           string
 	appTag           string
+	deploymentTarget string
 	appCpuMilli      int
 	appCpuMilli      int
 	appMemoryMi      int
 	appMemoryMi      int
 	jobName          string
 	jobName          string
@@ -58,6 +59,14 @@ func registerCommand_App(cliConf config.CLIConfig) *cobra.Command {
 		Short: "Runs a command for your application.",
 		Short: "Runs a command for your application.",
 	}
 	}
 
 
+	appCmd.PersistentFlags().StringVarP(
+		&deploymentTarget,
+		"target",
+		"x",
+		"default",
+		"the deployment target for the app, default is \"default\"",
+	)
+
 	// appRunCmd represents the "porter app run" subcommand
 	// appRunCmd represents the "porter app run" subcommand
 	appRunCmd := &cobra.Command{
 	appRunCmd := &cobra.Command{
 		Use:   "run [application] -- COMMAND [args...]",
 		Use:   "run [application] -- COMMAND [args...]",
@@ -238,7 +247,7 @@ func appRun(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client a
 	// updated exec args includes launcher command prepended if needed, otherwise it is the same as execArgs
 	// updated exec args includes launcher command prepended if needed, otherwise it is the same as execArgs
 	var updatedExecArgs []string
 	var updatedExecArgs []string
 	if project.ValidateApplyV2 {
 	if project.ValidateApplyV2 {
-		podsSimple, updatedExecArgs, namespace, err = getPodsFromV2PorterYaml(ctx, execArgs, client, cliConfig, args[0])
+		podsSimple, updatedExecArgs, namespace, err = getPodsFromV2PorterYaml(ctx, execArgs, client, cliConfig, args[0], deploymentTarget)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -553,23 +562,12 @@ func appGetPodsV1PorterYaml(ctx context.Context, cliConfig config.CLIConfig, cli
 	return res, containerHasLauncherStartCommand, nil
 	return res, containerHasLauncherStartCommand, nil
 }
 }
 
 
-func appGetPodsV2PorterYaml(ctx context.Context, cliConfig config.CLIConfig, client api.Client, porterAppName string) ([]appPodSimple, string, bool, error) {
+func appGetPodsV2PorterYaml(ctx context.Context, cliConfig config.CLIConfig, client api.Client, porterAppName string, deploymentTargetName string) ([]appPodSimple, string, bool, error) {
 	pID := cliConfig.Project
 	pID := cliConfig.Project
 	cID := cliConfig.Cluster
 	cID := cliConfig.Cluster
 	var containerHasLauncherStartCommand bool
 	var containerHasLauncherStartCommand bool
 
 
-	targetResp, err := client.DefaultDeploymentTarget(ctx, pID, cID)
-	if err != nil {
-		return nil, "", containerHasLauncherStartCommand, fmt.Errorf("error calling default deployment target endpoint: %w", err)
-	}
-
-	if targetResp.DeploymentTargetID == "" {
-		return nil, "", containerHasLauncherStartCommand, errors.New("deployment target id is empty")
-	}
-
-	resp, err := client.PorterYamlV2Pods(ctx, pID, cID, porterAppName, &types.PorterYamlV2PodsRequest{
-		DeploymentTargetID: targetResp.DeploymentTargetID,
-	})
+	resp, err := client.PorterYamlV2Pods(ctx, pID, cID, porterAppName, deploymentTargetName)
 	if err != nil {
 	if err != nil {
 		return nil, "", containerHasLauncherStartCommand, err
 		return nil, "", containerHasLauncherStartCommand, err
 	}
 	}
@@ -1215,7 +1213,7 @@ func appUpdateTag(ctx context.Context, user *types.GetAuthenticatedUserResponse,
 	}
 	}
 
 
 	if project.ValidateApplyV2 {
 	if project.ValidateApplyV2 {
-		tag, err := v2.UpdateImage(ctx, appTag, client, cliConf.Project, cliConf.Cluster, args[0])
+		tag, err := v2.UpdateImage(ctx, appTag, client, cliConf.Project, cliConf.Cluster, args[0], deploymentTarget)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("error updating tag: %w", err)
 			return fmt.Errorf("error updating tag: %w", err)
 		}
 		}
@@ -1276,8 +1274,8 @@ func getPodsFromV1PorterYaml(ctx context.Context, execArgs []string, client api.
 	return podsSimple, execArgs, nil
 	return podsSimple, execArgs, nil
 }
 }
 
 
-func getPodsFromV2PorterYaml(ctx context.Context, execArgs []string, client api.Client, cliConfig config.CLIConfig, porterAppName string) ([]appPodSimple, []string, string, error) {
-	podsSimple, namespace, containerHasLauncherStartCommand, err := appGetPodsV2PorterYaml(ctx, cliConfig, client, porterAppName)
+func getPodsFromV2PorterYaml(ctx context.Context, execArgs []string, client api.Client, cliConfig config.CLIConfig, porterAppName string, deploymentTargetName string) ([]appPodSimple, []string, string, error) {
+	podsSimple, namespace, containerHasLauncherStartCommand, err := appGetPodsV2PorterYaml(ctx, cliConfig, client, porterAppName, deploymentTargetName)
 	if err != nil {
 	if err != nil {
 		return nil, nil, "", fmt.Errorf("could not retrieve list of pods: %w", err)
 		return nil, nil, "", fmt.Errorf("could not retrieve list of pods: %w", err)
 	}
 	}

+ 165 - 0
cli/cmd/commands/env.go

@@ -0,0 +1,165 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/spf13/cobra"
+)
+
+var (
+	appName      string
+	envGroupName string
+	envFilePath  string
+)
+
+type envVariables struct {
+	Variables map[string]string `json:"variables"`
+	Secrets   map[string]string `json:"secrets"`
+}
+
+func registerCommand_Env(cliConf config.CLIConfig) *cobra.Command {
+	envCmd := &cobra.Command{
+		Use:   "env",
+		Args:  cobra.MinimumNArgs(1),
+		Short: "Manage environment variables for a project",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cmd.Help()
+		},
+	}
+
+	pullCommand := &cobra.Command{
+		Use:   "pull",
+		Short: "Pull environment variables for an app or environment group",
+		Long: `Pull environment variables for an app or environment group. 
+
+Optionally, specify a file to write the environment variables to. Otherwise the environment variables will be written to stdout.`,
+		Args: cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return checkLoginAndRunWithConfig(cmd, cliConf, args, pullEnv)
+		},
+	}
+
+	pullCommand.Flags().StringVarP(&appName, "app", "a", "", "app name")
+	pullCommand.Flags().StringVarP(&envGroupName, "group", "g", "", "environment group name")
+	pullCommand.Flags().StringVarP(&envFilePath, "file", "f", "", "file to write environment variables to")
+
+	envCmd.AddCommand(pullCommand)
+
+	return envCmd
+}
+
+func pullEnv(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
+	if appName == "" && envGroupName == "" {
+		return fmt.Errorf("must specify either --app or --group")
+	}
+	if appName != "" && envGroupName != "" {
+		return fmt.Errorf("only one of --app or --group can be specified")
+	}
+
+	var envVars envVariables
+
+	if appName != "" {
+		color.New(color.FgGreen).Printf("Pulling environment variables for app %s...\n", appName) // nolint:errcheck,gosec
+
+		envVarsResp, err := client.GetAppEnvVariables(ctx, cliConf.Project, cliConf.Cluster, appName)
+		if err != nil {
+			return fmt.Errorf("could not get app env variables: %w", err)
+		}
+		if envVarsResp == nil {
+			return fmt.Errorf("could not get app env variables: response was nil")
+		}
+
+		envVars = envVariables{
+			Variables: envVarsResp.EnvVariables.Variables,
+			Secrets:   envVarsResp.EnvVariables.Secrets,
+		}
+	}
+
+	if envGroupName != "" {
+		color.New(color.FgGreen).Printf("Pulling environment variables for environment group %s...\n", envGroupName) // nolint:errcheck,gosec
+
+		envVarsResp, err := client.GetLatestEnvGroupVariables(ctx, cliConf.Project, cliConf.Cluster, envGroupName)
+		if err != nil {
+			return fmt.Errorf("could not get env group env variables: %w", err)
+		}
+		if envVarsResp == nil {
+			return fmt.Errorf("could not get env group variables: response was nil")
+		}
+
+		envVars = envVariables{
+			Variables: envVarsResp.Variables,
+			Secrets:   envVarsResp.Secrets,
+		}
+	}
+
+	if envFilePath != "" {
+		err := writeEnvFile(envFilePath, envVars)
+		if err != nil {
+			return fmt.Errorf("could not write env file: %w", err)
+		}
+		color.New(color.FgGreen).Printf("Wrote environment variables to %s\n", envFilePath) // nolint:errcheck,gosec
+	}
+
+	if envFilePath == "" {
+		err := exportEnvVars(envVars)
+		if err != nil {
+			return fmt.Errorf("could not export env vars: %w", err)
+		}
+	}
+
+	return nil
+}
+
+func writeEnvFile(envFilePath string, envVars envVariables) error {
+	// open existing file or create new file: https://pkg.go.dev/os#example-OpenFile-Append
+	envFile, err := os.OpenFile(envFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) // nolint:gosec
+	if err != nil {
+		return err
+	}
+	defer envFile.Close() // nolint:errcheck
+
+	_, err = envFile.WriteString("# Generated by Porter CLI\n")
+	if err != nil {
+		return err
+	}
+
+	for k, v := range envVars.Variables {
+		_, err := envFile.WriteString(fmt.Sprintf("%s=%s\n", k, v))
+		if err != nil {
+			return err
+		}
+	}
+
+	for k, v := range envVars.Secrets {
+		_, err := envFile.WriteString(fmt.Sprintf("%s=%s\n", k, v))
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func exportEnvVars(envVars envVariables) error {
+	for k, v := range envVars.Variables {
+		_, err := os.Stdout.WriteString(fmt.Sprintf("%s=%s\n", k, v))
+		if err != nil {
+			return err
+		}
+	}
+
+	for k, v := range envVars.Secrets {
+		_, err := os.Stdout.WriteString(fmt.Sprintf("%s=%s\n", k, v))
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 4 - 9
cli/cmd/v2/update_image.go

@@ -9,21 +9,16 @@ import (
 )
 )
 
 
 // UpdateImage updates the image of an application
 // UpdateImage updates the image of an application
-func UpdateImage(ctx context.Context, tag string, client api.Client, projectId, clusterId uint, appName string) (string, error) {
-	targetResp, err := client.DefaultDeploymentTarget(ctx, projectId, clusterId)
-	if err != nil {
-		return "", fmt.Errorf("error calling default deployment target endpoint: %w", err)
-	}
-
-	if targetResp.DeploymentTargetID == "" {
-		return "", errors.New("deployment target id is empty")
+func UpdateImage(ctx context.Context, tag string, client api.Client, projectId, clusterId uint, appName string, deploymentTargetName string) (string, error) {
+	if deploymentTargetName == "" {
+		return "", errors.New("please provide a deployment target")
 	}
 	}
 
 
 	if tag == "" {
 	if tag == "" {
 		tag = "latest"
 		tag = "latest"
 	}
 	}
 
 
-	resp, err := client.UpdateImage(ctx, projectId, clusterId, appName, targetResp.DeploymentTargetID, tag)
+	resp, err := client.UpdateImage(ctx, projectId, clusterId, appName, deploymentTargetName, tag)
 	if err != nil {
 	if err != nil {
 		return "", fmt.Errorf("unable to update image: %w", err)
 		return "", fmt.Errorf("unable to update image: %w", err)
 	}
 	}

+ 82 - 7
dashboard/package-lock.json

@@ -47,6 +47,7 @@
         "dayjs": "^1.11.5",
         "dayjs": "^1.11.5",
         "deep-diff": "^1.0.2",
         "deep-diff": "^1.0.2",
         "dotenv": "^8.2.0",
         "dotenv": "^8.2.0",
+        "framer-motion": "^10.16.16",
         "fuse.js": "^6.6.2",
         "fuse.js": "^6.6.2",
         "ini": ">=1.3.6",
         "ini": ">=1.3.6",
         "js-base64": "^3.6.0",
         "js-base64": "^3.6.0",
@@ -92,7 +93,7 @@
         "@babel/preset-typescript": "^7.15.0",
         "@babel/preset-typescript": "^7.15.0",
         "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
         "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
         "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
         "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-        "@porter-dev/api-contracts": "^0.2.68",
+        "@porter-dev/api-contracts": "^0.2.71",
         "@testing-library/jest-dom": "^4.2.4",
         "@testing-library/jest-dom": "^4.2.4",
         "@testing-library/react": "^9.3.2",
         "@testing-library/react": "^9.3.2",
         "@testing-library/user-event": "^7.1.2",
         "@testing-library/user-event": "^7.1.2",
@@ -2660,9 +2661,9 @@
       }
       }
     },
     },
     "node_modules/@porter-dev/api-contracts": {
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.2.68",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.68.tgz",
-      "integrity": "sha512-cNexjW/HD1O68e9anCrAF9TR4HSqmKFiz2dB9E2exO0IFAG/pWW6DvfFF+0TSCstmtHOnE+x/K2F7MNmaMocqQ==",
+      "version": "0.2.71",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.71.tgz",
+      "integrity": "sha512-fVYGLym26GAEcvOw0hPsbhDkge1InN8+a15papXkOA4R/AOmkUsVA1Hn2zgrFosTwF2matXKh+MPYoQVbbXTYA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
         "@bufbuild/protobuf": "^1.1.0"
@@ -9103,6 +9104,49 @@
         "node": ">=0.10.0"
         "node": ">=0.10.0"
       }
       }
     },
     },
+    "node_modules/framer-motion": {
+      "version": "10.16.16",
+      "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.16.tgz",
+      "integrity": "sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==",
+      "dependencies": {
+        "tslib": "^2.4.0"
+      },
+      "optionalDependencies": {
+        "@emotion/is-prop-valid": "^0.8.2"
+      },
+      "peerDependencies": {
+        "react": "^18.0.0",
+        "react-dom": "^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": {
+      "version": "0.8.8",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+      "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+      "optional": true,
+      "dependencies": {
+        "@emotion/memoize": "0.7.4"
+      }
+    },
+    "node_modules/framer-motion/node_modules/@emotion/memoize": {
+      "version": "0.7.4",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+      "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
+      "optional": true
+    },
+    "node_modules/framer-motion/node_modules/tslib": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+    },
     "node_modules/fresh": {
     "node_modules/fresh": {
       "version": "0.5.2",
       "version": "0.5.2",
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -19842,9 +19886,9 @@
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     },
     "@porter-dev/api-contracts": {
     "@porter-dev/api-contracts": {
-      "version": "0.2.68",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.68.tgz",
-      "integrity": "sha512-cNexjW/HD1O68e9anCrAF9TR4HSqmKFiz2dB9E2exO0IFAG/pWW6DvfFF+0TSCstmtHOnE+x/K2F7MNmaMocqQ==",
+      "version": "0.2.71",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.71.tgz",
+      "integrity": "sha512-fVYGLym26GAEcvOw0hPsbhDkge1InN8+a15papXkOA4R/AOmkUsVA1Hn2zgrFosTwF2matXKh+MPYoQVbbXTYA==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"
         "@bufbuild/protobuf": "^1.1.0"
@@ -25064,6 +25108,37 @@
         "map-cache": "^0.2.2"
         "map-cache": "^0.2.2"
       }
       }
     },
     },
+    "framer-motion": {
+      "version": "10.16.16",
+      "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.16.tgz",
+      "integrity": "sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==",
+      "requires": {
+        "@emotion/is-prop-valid": "^0.8.2",
+        "tslib": "^2.4.0"
+      },
+      "dependencies": {
+        "@emotion/is-prop-valid": {
+          "version": "0.8.8",
+          "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+          "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+          "optional": true,
+          "requires": {
+            "@emotion/memoize": "0.7.4"
+          }
+        },
+        "@emotion/memoize": {
+          "version": "0.7.4",
+          "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+          "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
+          "optional": true
+        },
+        "tslib": {
+          "version": "2.6.2",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+          "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+        }
+      }
+    },
     "fresh": {
     "fresh": {
       "version": "0.5.2",
       "version": "0.5.2",
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",

+ 2 - 1
dashboard/package.json

@@ -42,6 +42,7 @@
     "dayjs": "^1.11.5",
     "dayjs": "^1.11.5",
     "deep-diff": "^1.0.2",
     "deep-diff": "^1.0.2",
     "dotenv": "^8.2.0",
     "dotenv": "^8.2.0",
+    "framer-motion": "^10.16.16",
     "fuse.js": "^6.6.2",
     "fuse.js": "^6.6.2",
     "ini": ">=1.3.6",
     "ini": ">=1.3.6",
     "js-base64": "^3.6.0",
     "js-base64": "^3.6.0",
@@ -99,7 +100,7 @@
     "@babel/preset-typescript": "^7.15.0",
     "@babel/preset-typescript": "^7.15.0",
     "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
     "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
     "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
     "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-    "@porter-dev/api-contracts": "^0.2.68",
+    "@porter-dev/api-contracts": "^0.2.71",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.3.2",
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
     "@testing-library/user-event": "^7.1.2",

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
dashboard/src/assets/postgresql.svg


+ 4 - 1
dashboard/src/components/CloudFormationForm.tsx

@@ -222,7 +222,10 @@ const CloudFormationForm: React.FC<Props> = ({
     setCurrentStep(3)
     setCurrentStep(3)
     const externalId = getExternalId();
     const externalId = getExternalId();
     let trustArn = process.env.TRUST_ARN ? process.env.TRUST_ARN : "arn:aws:iam::108458755588:role/CAPIManagement";
     let trustArn = process.env.TRUST_ARN ? process.env.TRUST_ARN : "arn:aws:iam::108458755588:role/CAPIManagement";
-    const cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole&param_ExternalIdParameter=${externalId}&param_TrustArnParameter=${trustArn}`
+    let cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole&param_ExternalIdParameter=${externalId}&param_TrustArnParameter=${trustArn}`
+    if (currentProject.aws_ack_auth_enabled === true) {
+      cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-access-policy.json&stackName=PorterRole&param_TrustArnParameter=${trustArn}`
+    }
     markStepStarted({ step: "aws-cloudformation-redirect-success", account_id: AWSAccountID, cloudformation_url, external_id: externalId })
     markStepStarted({ step: "aws-cloudformation-redirect-success", account_id: AWSAccountID, cloudformation_url, external_id: externalId })
     window.open(cloudformation_url, "_blank")
     window.open(cloudformation_url, "_blank")
     setHasClickedCloudformationButton(true);
     setHasClickedCloudformationButton(true);

+ 65 - 0
dashboard/src/lib/addons/index.ts

@@ -0,0 +1,65 @@
+import {
+  Addon,
+  AddonType,
+} from "@porter-dev/api-contracts/src/porter/v1/addons_pb";
+import { match } from "ts-pattern";
+import { z } from "zod";
+
+import { serviceStringValidator } from "lib/porter-apps/values";
+
+import { defaultPostgresAddon, postgresConfigValidator } from "./postgres";
+
+export const clientAddonValidator = z.object({
+  expanded: z.boolean().default(true),
+  canDelete: z.boolean().default(true),
+  name: z.object({
+    readOnly: z.boolean(),
+    value: z
+      .string()
+      .min(1, { message: "Name must be at least 1 character" })
+      .max(31, { message: "Name must be 31 characters or less" })
+      .regex(/^[a-z0-9-]{1,61}$/, {
+        message: 'Lowercase letters, numbers, and "-" only.',
+      }),
+  }),
+  envGroups: z.array(serviceStringValidator).default([]),
+  config: z.discriminatedUnion("type", [postgresConfigValidator]),
+});
+export type ClientAddon = z.infer<typeof clientAddonValidator>;
+
+export function defaultClientAddon(): ClientAddon {
+  return clientAddonValidator.parse({
+    name: { readOnly: false, value: "addon" },
+    config: defaultPostgresAddon(),
+  });
+}
+
+function addonTypeEnumProto(type: ClientAddon["config"]["type"]): AddonType {
+  return match(type)
+    .with("postgres", () => AddonType.POSTGRES)
+    .exhaustive();
+}
+
+export function clientAddonToProto(addon: ClientAddon): Addon {
+  const config = match(addon.config)
+    .with({ type: "postgres" }, (data) => ({
+      value: {
+        cpuCores: data.cpuCores.value,
+        ramMegabytes: data.ramMegabytes.value,
+        storageGigabytes: data.storageGigabytes.value,
+      },
+      case: "postgres" as const,
+    }))
+    .exhaustive();
+
+  const proto = new Addon({
+    name: addon.name.value,
+    type: addonTypeEnumProto(addon.config.type),
+    envGroups: addon.envGroups.map((envGroup) => ({
+      name: envGroup.value,
+    })),
+    config,
+  });
+
+  return proto;
+}

+ 28 - 0
dashboard/src/lib/addons/postgres.ts

@@ -0,0 +1,28 @@
+import { z } from "zod";
+
+import { serviceNumberValidator } from "lib/porter-apps/values";
+
+export const postgresConfigValidator = z.object({
+  type: z.literal("postgres"),
+  cpuCores: serviceNumberValidator.default({
+    value: 0.5,
+    readOnly: false,
+  }),
+  ramMegabytes: serviceNumberValidator.default({
+    value: 512,
+    readOnly: false,
+  }),
+  storageGigabytes: serviceNumberValidator.default({
+    value: 1,
+    readOnly: false,
+  }),
+  username: z.string().default("postgres"),
+  password: z.string().default("postgres"),
+});
+export type PostgresConfig = z.infer<typeof postgresConfigValidator>;
+
+export function defaultPostgresAddon(): PostgresConfig {
+  return postgresConfigValidator.parse({
+    type: "postgres",
+  });
+}

+ 61 - 0
dashboard/src/lib/hooks/useAppWithPreviewOverrides.ts

@@ -0,0 +1,61 @@
+import { useMemo } from "react";
+import { PorterApp } from "@porter-dev/api-contracts";
+
+import { type PopulatedEnvGroup } from "main/home/app-dashboard/validate-apply/app-settings/types";
+import {
+  applyPreviewOverrides,
+  clientAppFromProto,
+  type ClientPorterApp,
+} from "lib/porter-apps";
+import { type DetectedServices } from "lib/porter-apps/services";
+
+export const useAppWithPreviewOverrides = ({
+  latestApp,
+  detectedServices,
+  templateEnv,
+  existingTemplate,
+  appEnv,
+}: {
+  latestApp: PorterApp;
+  detectedServices: DetectedServices | null;
+  existingTemplate?: PorterApp;
+  templateEnv?: {
+    variables: Record<string, string>;
+    secret_variables: Record<string, string>;
+  };
+  appEnv?: PopulatedEnvGroup;
+}): ClientPorterApp => {
+  const withPreviewOverrides = useMemo(() => {
+    const proto =
+      existingTemplate ||
+      new PorterApp({
+        ...latestApp,
+        envGroups: [],
+      }); // clear out env groups, they won't get added to the template anyways
+
+    const variables = templateEnv ? templateEnv.variables : appEnv?.variables;
+    const secrets = templateEnv
+      ? templateEnv.secret_variables
+      : appEnv?.secret_variables;
+
+    return applyPreviewOverrides({
+      app: clientAppFromProto({
+        proto,
+        overrides: detectedServices,
+        variables,
+        secrets,
+        lockServiceDeletions: true,
+      }),
+      overrides: detectedServices?.previews,
+    });
+  }, [
+    latestApp,
+    detectedServices,
+    existingTemplate,
+    templateEnv,
+    appEnv?.variables,
+    appEnv?.secret_variables,
+  ]);
+
+  return withPreviewOverrides;
+};

+ 21 - 7
dashboard/src/lib/porter-apps/index.ts

@@ -98,17 +98,19 @@ export const clientAppValidator = z.object({
     .default([]),
     .default([]),
   build: buildValidator,
   build: buildValidator,
   helmOverrides: z.string().optional(),
   helmOverrides: z.string().optional(),
+  requiredApps: z.object({ name: z.string() }).array().default([]),
 });
 });
 export type ClientPorterApp = z.infer<typeof clientAppValidator>;
 export type ClientPorterApp = z.infer<typeof clientAppValidator>;
 
 
+export const basePorterAppFormValidator = z.object({
+  app: clientAppValidator,
+  source: sourceValidator,
+  deletions: deletionValidator,
+  redeployOnSave: z.boolean().default(false),
+});
+
 // porterAppFormValidator is used to validate inputs when creating + updating an app
 // porterAppFormValidator is used to validate inputs when creating + updating an app
-export const porterAppFormValidator = z
-  .object({
-    app: clientAppValidator,
-    source: sourceValidator,
-    deletions: deletionValidator,
-    redeployOnSave: z.boolean().default(false),
-  })
+export const porterAppFormValidator = basePorterAppFormValidator
   .refine(
   .refine(
     ({ app }) => {
     ({ app }) => {
       if (app.predeploy?.[0]?.run) {
       if (app.predeploy?.[0]?.run) {
@@ -316,6 +318,9 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
           efsStorage: new EFS({
           efsStorage: new EFS({
             enabled: app.efsStorage.enabled,
             enabled: app.efsStorage.enabled,
           }),
           }),
+          requiredApps: app.requiredApps.map((app) => ({
+            name: app.name,
+          })),
         })
         })
     )
     )
     .with(
     .with(
@@ -339,6 +344,9 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
           efsStorage: new EFS({
           efsStorage: new EFS({
             enabled: app.efsStorage.enabled,
             enabled: app.efsStorage.enabled,
           }),
           }),
+          requiredApps: app.requiredApps.map((app) => ({
+            name: app.name,
+          })),
         })
         })
     )
     )
     .exhaustive();
     .exhaustive();
@@ -486,6 +494,9 @@ export function clientAppFromProto({
       efsStorage: new EFS({
       efsStorage: new EFS({
         enabled: proto.efsStorage?.enabled ?? false,
         enabled: proto.efsStorage?.enabled ?? false,
       }),
       }),
+      requiredApps: proto.requiredApps.map((app) => ({
+        name: app.name,
+      })),
     };
     };
   }
   }
 
 
@@ -525,6 +536,9 @@ export function clientAppFromProto({
     },
     },
     helmOverrides,
     helmOverrides,
     efsStorage: { enabled: proto.efsStorage?.enabled ?? false },
     efsStorage: { enabled: proto.efsStorage?.enabled ?? false },
+    requiredApps: proto.requiredApps.map((app) => ({
+      name: app.name,
+    })),
   };
   };
 }
 }
 
 

+ 20 - 18
dashboard/src/main/home/app-dashboard/app-view/tabs/Environment.tsx

@@ -1,17 +1,19 @@
+import React from "react";
+import { useQuery } from "@tanstack/react-query";
+import { useFormContext } from "react-hook-form";
+import { z } from "zod";
+
+import Button from "components/porter/Button";
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
-import React, { useMemo } from "react";
-import Button from "components/porter/Button";
-import Error from "components/porter/Error";
-import { useFormContext } from "react-hook-form";
-import { PorterAppFormData, SourceOptions } from "lib/porter-apps";
-import { useLatestRevision } from "../LatestRevisionContext";
-import { useQuery } from "@tanstack/react-query";
+import { type PorterAppFormData, type SourceOptions } from "lib/porter-apps";
+
 import api from "shared/api";
 import api from "shared/api";
-import { z } from "zod";
-import { populatedEnvGroup } from "../../validate-apply/app-settings/types";
+
 import EnvSettings from "../../validate-apply/app-settings/EnvSettings";
 import EnvSettings from "../../validate-apply/app-settings/EnvSettings";
-import { ButtonStatus } from "../AppDataContainer";
+import { populatedEnvGroup } from "../../validate-apply/app-settings/types";
+import { type ButtonStatus } from "../AppDataContainer";
+import { useLatestRevision } from "../LatestRevisionContext";
 
 
 type Props = {
 type Props = {
   latestSource: SourceOptions;
   latestSource: SourceOptions;
@@ -43,13 +45,13 @@ const Environment: React.FC<Props> = ({ latestSource, buttonStatus }) => {
         }
         }
       );
       );
 
 
-      const { environment_groups } = await z
+      const { environment_groups: envGroups } = await z
         .object({
         .object({
           environment_groups: z.array(populatedEnvGroup).default([]),
           environment_groups: z.array(populatedEnvGroup).default([]),
         })
         })
         .parseAsync(res.data);
         .parseAsync(res.data);
 
 
-      return environment_groups;
+      return envGroups;
     }
     }
   );
   );
 
 
@@ -57,10 +59,13 @@ const Environment: React.FC<Props> = ({ latestSource, buttonStatus }) => {
     <>
     <>
       <Text size={16}>Environment variables</Text>
       <Text size={16}>Environment variables</Text>
       <Spacer y={0.5} />
       <Spacer y={0.5} />
-      <Text color="helper">Shared among all services. All non-secret variables are also available at build time.</Text>
+      <Text color="helper">
+        Shared among all services. All non-secret variables are also available
+        at build time.
+      </Text>
       <EnvSettings
       <EnvSettings
         appName={latestProto.name}
         appName={latestProto.name}
-        revision={previewRevision ? previewRevision : latestRevision} // get versions of env groups attached to preview revision if set
+        revision={previewRevision || latestRevision} // get versions of env groups attached to preview revision if set
         baseEnvGroups={baseEnvGroups}
         baseEnvGroups={baseEnvGroups}
         latestSource={latestSource}
         latestSource={latestSource}
         attachedEnvGroups={attachedEnvGroups}
         attachedEnvGroups={attachedEnvGroups}
@@ -70,10 +75,7 @@ const Environment: React.FC<Props> = ({ latestSource, buttonStatus }) => {
         type="submit"
         type="submit"
         status={buttonStatus}
         status={buttonStatus}
         loadingText={"Updating..."}
         loadingText={"Updating..."}
-        disabled={
-          isSubmitting ||
-          latestRevision.status === "CREATED"
-        }
+        disabled={isSubmitting || latestRevision.status === "CREATED"}
         disabledTooltipMessage="Please wait for the deploy to complete before updating environment variables"
         disabledTooltipMessage="Please wait for the deploy to complete before updating environment variables"
       >
       >
         Update app
         Update app

+ 31 - 39
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx

@@ -1,13 +1,12 @@
-import React, { useCallback, useEffect, useState } from "react";
+import React from "react";
+import { AnimatePresence, motion } from "framer-motion";
 import _ from "lodash";
 import _ from "lodash";
-import AnimateHeight, { type Height } from "react-animate-height";
 import { type UseFieldArrayUpdate } from "react-hook-form";
 import { type UseFieldArrayUpdate } from "react-hook-form";
 import styled, { keyframes } from "styled-components";
 import styled, { keyframes } from "styled-components";
 import { match } from "ts-pattern";
 import { match } from "ts-pattern";
 
 
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
 import { type ClientServiceStatus } from "lib/hooks/useAppStatus";
 import { type ClientServiceStatus } from "lib/hooks/useAppStatus";
-import useResizeObserver from "lib/hooks/useResizeObserver";
 import { type PorterAppFormData } from "lib/porter-apps";
 import { type PorterAppFormData } from "lib/porter-apps";
 import { type ClientService } from "lib/porter-apps/services";
 import { type ClientService } from "lib/porter-apps/services";
 
 
@@ -54,28 +53,6 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   clusterIngressIp,
   clusterIngressIp,
   showDisableTls,
   showDisableTls,
 }) => {
 }) => {
-  const [height, setHeight] = useState<Height>(service.expanded ? "auto" : 0);
-
-  // onResize is called when the height of the service container changes
-  // used to set the height of the AnimateHeight component on tab swtich
-  const onResize = useCallback(
-    (elt: HTMLDivElement) => {
-      if (elt.clientHeight === 0) {
-        return;
-      }
-
-      setHeight(elt.clientHeight ?? "auto");
-    },
-    [setHeight]
-  );
-  const ref = useResizeObserver(onResize);
-
-  useEffect(() => {
-    if (!service.expanded) {
-      setHeight(0);
-    }
-  }, [service.expanded]);
-
   const renderTabs = (service: ClientService): JSX.Element => {
   const renderTabs = (service: ClientService): JSX.Element => {
     return match(service)
     return match(service)
       .with({ config: { type: "web" } }, (svc) => (
       .with({ config: { type: "web" } }, (svc) => (
@@ -176,21 +153,39 @@ const ServiceContainer: React.FC<ServiceProps> = ({
           </ActionButton>
           </ActionButton>
         )}
         )}
       </ServiceHeader>
       </ServiceHeader>
-      <AnimateHeight
-        height={height}
-        contentRef={ref}
-        contentClassName="auto-content"
-        duration={300}
-      >
-        {height !== 0 && (
+      <AnimatePresence>
+        {service.expanded && (
           <StyledSourceBox
           <StyledSourceBox
+            key={service.name.value}
+            initial={{
+              height: 0,
+            }}
+            animate={{
+              height: "auto",
+              transition: {
+                duration: 0.3,
+              },
+            }}
+            exit={{
+              height: 0,
+              transition: {
+                duration: 0.3,
+              },
+            }}
             showExpanded={service.expanded}
             showExpanded={service.expanded}
             hasFooter={status != null}
             hasFooter={status != null}
           >
           >
-            {renderTabs(service)}
+            <div
+              style={{
+                padding: "14px 25px 30px",
+                border: "1px solid #494b4f",
+              }}
+            >
+              {renderTabs(service)}
+            </div>
           </StyledSourceBox>
           </StyledSourceBox>
         )}
         )}
-      </AnimateHeight>
+      </AnimatePresence>
       {status && (
       {status && (
         <ServiceStatusFooter
         <ServiceStatusFooter
           serviceName={service.name.value}
           serviceName={service.name.value}
@@ -210,17 +205,14 @@ const ServiceTitle = styled.div`
   align-items: center;
   align-items: center;
 `;
 `;
 
 
-const StyledSourceBox = styled.div<{
+const StyledSourceBox = styled(motion.div)<{
   showExpanded?: boolean;
   showExpanded?: boolean;
   hasFooter?: boolean;
   hasFooter?: boolean;
 }>`
 }>`
-  width: 100%;
+  overflow: hidden;
   color: #ffffff;
   color: #ffffff;
-  padding: 14px 25px 30px;
-  position: relative;
   font-size: 13px;
   font-size: 13px;
   background: ${(props) => props.theme.fg};
   background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
   border-top: 0;
   border-top: 0;
   border-bottom-left-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
   border-bottom-left-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
   border-bottom-right-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
   border-bottom-right-radius: ${(props) => (props.hasFooter ? "0" : "5px")};

+ 37 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/Addons.tsx

@@ -0,0 +1,37 @@
+import React from "react";
+import _ from "lodash";
+import { useFormContext } from "react-hook-form";
+
+import Button from "components/porter/Button";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { type ButtonStatus } from "main/home/app-dashboard/app-view/AppDataContainer";
+import { AddonsList } from "main/home/managed-addons/AddonsList";
+import { type PorterAppFormData } from "lib/porter-apps";
+
+type Props = {
+  buttonStatus: ButtonStatus;
+};
+
+export const Addons: React.FC<Props> = ({ buttonStatus }) => {
+  const {
+    formState: { isSubmitting },
+  } = useFormContext<PorterAppFormData>();
+
+  return (
+    <>
+      <Text size={16}>Add-ons</Text>
+      <Spacer y={0.5} />
+      <AddonsList />
+      <Spacer y={0.75} />
+      <Button
+        type="submit"
+        status={buttonStatus}
+        loadingText={"Updating..."}
+        disabled={isSubmitting}
+      >
+        Update app
+      </Button>
+    </>
+  );
+};

+ 125 - 136
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/AppTemplateForm.tsx → dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx

@@ -1,51 +1,72 @@
-import React, { useCallback, useEffect, useMemo, useState } from "react";
+import React, {
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+  useState,
+} from "react";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { zodResolver } from "@hookform/resolvers/zod";
-import { PorterApp } from "@porter-dev/api-contracts";
-import { useQuery } from "@tanstack/react-query";
+import { type PorterApp } from "@porter-dev/api-contracts";
 import axios from "axios";
 import axios from "axios";
 import _ from "lodash";
 import _ from "lodash";
 import { FormProvider, useForm } from "react-hook-form";
 import { FormProvider, useForm } from "react-hook-form";
 import { Redirect, useHistory } from "react-router";
 import { Redirect, useHistory } from "react-router";
+import { match } from "ts-pattern";
 import { z } from "zod";
 import { z } from "zod";
 
 
-import Button from "components/porter/Button";
 import Error from "components/porter/Error";
 import Error from "components/porter/Error";
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
-import VerticalSteps from "components/porter/VerticalSteps";
+import TabSelector from "components/TabSelector";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
+import Environment from "main/home/app-dashboard/app-view/tabs/Environment";
 import GithubActionModal from "main/home/app-dashboard/new-app-flow/GithubActionModal";
 import GithubActionModal from "main/home/app-dashboard/new-app-flow/GithubActionModal";
-import EnvSettings from "main/home/app-dashboard/validate-apply/app-settings/EnvSettings";
-import { populatedEnvGroup } from "main/home/app-dashboard/validate-apply/app-settings/types";
-import ServiceList from "main/home/app-dashboard/validate-apply/services-settings/ServiceList";
+import { clientAddonToProto, clientAddonValidator } from "lib/addons";
+import { useAppWithPreviewOverrides } from "lib/hooks/useAppWithPreviewOverrides";
 import {
 import {
-  applyPreviewOverrides,
-  clientAppFromProto,
+  basePorterAppFormValidator,
   clientAppToProto,
   clientAppToProto,
-  porterAppFormValidator,
-  type PorterAppFormData,
   type SourceOptions,
   type SourceOptions,
 } from "lib/porter-apps";
 } from "lib/porter-apps";
-import {
-  defaultSerialized,
-  deserializeService,
-} from "lib/porter-apps/services";
 
 
 import api from "shared/api";
 import api from "shared/api";
-import { useClusterResources } from "shared/ClusterResourcesContext";
+import { Context } from "shared/Context";
+
+import { type ExistingTemplateWithEnv } from "../types";
+import { Addons } from "./Addons";
+import { RequiredApps } from "./RequiredApps";
+import { ServiceSettings } from "./ServiceSettings";
 
 
 type Props = {
 type Props = {
-  existingTemplate: {
-    template: PorterApp;
-    env: {
-      variables: Record<string, string>;
-      secret_variables: Record<string, string>;
-    };
-  } | null;
+  existingTemplate: ExistingTemplateWithEnv | null;
+};
+
+const previewEnvSettingsTabs = [
+  "services",
+  "variables",
+  "addons",
+  "required-apps",
+] as const;
+
+type PreviewEnvSettingsTab = (typeof previewEnvSettingsTabs)[number];
+
+const appTemplateClientValidator = basePorterAppFormValidator.extend({
+  addons: z.array(clientAddonValidator).default([]),
+});
+export type AppTemplateFormData = z.infer<typeof appTemplateClientValidator>;
+
+type EncodedAddonWithEnv = {
+  base64_addon: string;
+  variables: Record<string, string>;
+  secrets: Record<string, string>;
 };
 };
 
 
-const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
+export const PreviewAppDataContainer: React.FC<Props> = ({
+  existingTemplate,
+}) => {
   const history = useHistory();
   const history = useHistory();
+  const { currentProject } = useContext(Context);
+
+  const [tab, setTab] = useState<PreviewEnvSettingsTab>("services");
   const [validatedAppProto, setValidatedAppProto] = useState<PorterApp | null>(
   const [validatedAppProto, setValidatedAppProto] = useState<PorterApp | null>(
     null
     null
   );
   );
@@ -58,7 +79,7 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
     variables: {},
     variables: {},
     secrets: {},
     secrets: {},
   });
   });
-  const { currentClusterResources } = useClusterResources();
+  const [encodedAddons, setEncodedAddons] = useState<EncodedAddonWithEnv[]>([]);
 
 
   const {
   const {
     porterApp,
     porterApp,
@@ -70,28 +91,6 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
     deploymentTarget,
     deploymentTarget,
   } = useLatestRevision();
   } = useLatestRevision();
 
 
-  const { data: baseEnvGroups = [] } = useQuery(
-    ["getAllEnvGroups", projectId, clusterId],
-    async () => {
-      const res = await api.getAllEnvGroups(
-        "<token>",
-        {},
-        {
-          id: projectId,
-          cluster_id: clusterId,
-        }
-      );
-
-      const { environment_groups: envGroups } = await z
-        .object({
-          environment_groups: z.array(populatedEnvGroup).default([]),
-        })
-        .parseAsync(res.data);
-
-      return envGroups;
-    }
-  );
-
   const latestSource: SourceOptions = useMemo(() => {
   const latestSource: SourceOptions = useMemo(() => {
     if (porterApp.image_repo_uri) {
     if (porterApp.image_repo_uri) {
       const [repository, tag] = porterApp.image_repo_uri.split(":");
       const [repository, tag] = porterApp.image_repo_uri.split(":");
@@ -113,31 +112,17 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
     };
     };
   }, [porterApp]);
   }, [porterApp]);
 
 
-  const withPreviewOverrides = useMemo(() => {
-    return applyPreviewOverrides({
-      app: clientAppFromProto({
-        proto: existingTemplate?.template
-          ? existingTemplate.template
-          : new PorterApp({
-              ...latestProto,
-              envGroups: [],
-            }), // clear out env groups, they won't get added to the template anyways
-        overrides: servicesFromYaml,
-        variables: existingTemplate
-          ? existingTemplate.env.variables
-          : appEnv?.variables,
-        secrets: existingTemplate
-          ? existingTemplate.env.secret_variables
-          : appEnv?.secret_variables,
-        lockServiceDeletions: true,
-      }),
-      overrides: servicesFromYaml?.previews,
-    });
-  }, [latestProto, existingTemplate?.template, appEnv, servicesFromYaml]);
+  const withPreviewOverrides = useAppWithPreviewOverrides({
+    latestApp: latestProto,
+    detectedServices: servicesFromYaml,
+    existingTemplate: existingTemplate?.template,
+    templateEnv: existingTemplate?.env,
+    appEnv,
+  });
 
 
-  const porterAppFormMethods = useForm<PorterAppFormData>({
+  const porterAppFormMethods = useForm<AppTemplateFormData>({
     reValidateMode: "onSubmit",
     reValidateMode: "onSubmit",
-    resolver: zodResolver(porterAppFormValidator),
+    resolver: zodResolver(appTemplateClientValidator),
     defaultValues: {
     defaultValues: {
       app: withPreviewOverrides,
       app: withPreviewOverrides,
       source: latestSource,
       source: latestSource,
@@ -182,6 +167,28 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
       const proto = clientAppToProto(data);
       const proto = clientAppToProto(data);
       setValidatedAppProto(proto);
       setValidatedAppProto(proto);
 
 
+      const addons = data.addons.map((addon) => {
+        const variables = match(addon.config.type)
+          .with("postgres", () => ({
+            POSTGRESQL_USERNAME: addon.config.username,
+          }))
+          .otherwise(() => ({}));
+        const secrets = match(addon.config.type)
+          .with("postgres", () => ({
+            POSTGRESQL_PASSWORD: addon.config.password,
+          }))
+          .otherwise(() => ({}));
+
+        const proto = clientAddonToProto(addon);
+
+        return {
+          base64_addon: btoa(proto.toJsonString()),
+          variables,
+          secrets,
+        };
+      });
+      setEncodedAddons(addons);
+
       const { env } = data.app;
       const { env } = data.app;
       const variables = env
       const variables = env
         .filter((e) => !e.hidden && !e.deleted)
         .filter((e) => !e.hidden && !e.deleted)
@@ -208,6 +215,7 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
         app: proto,
         app: proto,
         variables,
         variables,
         secrets,
         secrets,
+        addons,
       });
       });
       history.push(`/preview-environments`);
       history.push(`/preview-environments`);
     } catch (err) {
     } catch (err) {
@@ -226,10 +234,12 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
       app,
       app,
       variables,
       variables,
       secrets,
       secrets,
+      addons = [],
     }: {
     }: {
       app: PorterApp | null;
       app: PorterApp | null;
       variables: Record<string, string>;
       variables: Record<string, string>;
       secrets: Record<string, string>;
       secrets: Record<string, string>;
+      addons?: EncodedAddonWithEnv[];
     }) => {
     }) => {
       try {
       try {
         if (!app) {
         if (!app) {
@@ -243,6 +253,7 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
             variables,
             variables,
             secrets,
             secrets,
             base_deployment_target_id: deploymentTarget.id,
             base_deployment_target_id: deploymentTarget.id,
+            addons,
           },
           },
           {
           {
             project_id: projectId,
             project_id: projectId,
@@ -285,69 +296,48 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
 
 
   return (
   return (
     <FormProvider {...porterAppFormMethods}>
     <FormProvider {...porterAppFormMethods}>
+      <TabSelector
+        noBuffer
+        options={[
+          { label: "App Services", value: "services" },
+          { label: "Environment Variables", value: "variables" },
+          ...(currentProject?.beta_features_enabled
+            ? [
+                // { label: "Required Apps", value: "required-apps" },
+                // { label: "Add-ons", value: "addons" },
+              ]
+            : []),
+        ]}
+        currentTab={tab}
+        setCurrentTab={(tab: string) => {
+          if (tab === "services") {
+            setTab("services");
+          } else if (tab === "variables") {
+            setTab("variables");
+          } else if (tab === "required-apps") {
+            setTab("required-apps");
+          } else {
+            setTab("addons");
+          }
+        }}
+      />
+      <Spacer y={1} />
       <form onSubmit={onSubmit}>
       <form onSubmit={onSubmit}>
-        <VerticalSteps
-          currentStep={3}
-          steps={[
-            <>
-              <Text size={16}>Application services</Text>
-              <Spacer y={0.5} />
-              <ServiceList
-                addNewText={"Add a new service"}
-                fieldArrayName={"app.services"}
-                internalNetworkingDetails={{
-                  namespace: deploymentTarget.namespace,
-                  appName: porterApp.name,
-                }}
-                allowAddServices={false}
-              />
-            </>,
-            <>
-              <Text size={16}>Environment variables (optional)</Text>
-              <Spacer y={0.5} />
-              <Text color="helper">
-                Specify environment variables shared among all services.
-              </Text>
-              <EnvSettings baseEnvGroups={baseEnvGroups} />
-            </>,
-            <>
-              <Text size={16}>Pre-deploy job (optional)</Text>
-              <Spacer y={0.5} />
-              <Text color="helper">
-                You may add a pre-deploy job to perform an operation before your
-                application services deploy each time, like a database
-                migration.
-              </Text>
-              <Spacer y={0.5} />
-              <ServiceList
-                addNewText={"Add a new pre-deploy job"}
-                prePopulateService={deserializeService({
-                  service: defaultSerialized({
-                    name: "pre-deploy",
-                    type: "predeploy",
-                    defaultCPU: currentClusterResources.defaultCPU,
-                    defaultRAM: currentClusterResources.defaultRAM,
-                  }),
-                })}
-                existingServiceNames={
-                  latestProto.predeploy ? ["pre-deploy"] : []
-                }
-                isPredeploy
-                fieldArrayName={"app.predeploy"}
-              />
-            </>,
-            <>
-              <Button
-                type="submit"
-                loadingText={"Saving..."}
-                width={"150px"}
-                status={buttonStatus}
-              >
-                {existingTemplate ? "Update Previews" : "Enable Previews"}
-              </Button>
-            </>,
-          ].filter((x) => x)}
-        />
+        {match(tab)
+          .with("services", () => (
+            <ServiceSettings buttonStatus={buttonStatus} />
+          ))
+          .with("variables", () => (
+            <Environment
+              latestSource={latestSource}
+              buttonStatus={buttonStatus}
+            />
+          ))
+          .with("required-apps", () => (
+            <RequiredApps buttonStatus={buttonStatus} />
+          ))
+          .with("addons", () => <Addons buttonStatus={buttonStatus} />)
+          .exhaustive()}
       </form>
       </form>
       {showGHAModal && (
       {showGHAModal && (
         <GithubActionModal
         <GithubActionModal
@@ -367,6 +357,7 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
               app: validatedAppProto,
               app: validatedAppProto,
               variables,
               variables,
               secrets,
               secrets,
+              addons: encodedAddons,
             })
             })
           }
           }
           deploymentError={createError}
           deploymentError={createError}
@@ -377,5 +368,3 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
     </FormProvider>
     </FormProvider>
   );
   );
 };
 };
-
-export default AppTemplateForm;

+ 203 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/RequiredApps.tsx

@@ -0,0 +1,203 @@
+import React, { useContext, useMemo } from "react";
+import { PorterApp } from "@porter-dev/api-contracts";
+import { useQuery } from "@tanstack/react-query";
+import {
+  useFieldArray,
+  useFormContext,
+  type UseFieldArrayAppend,
+} from "react-hook-form";
+import styled from "styled-components";
+import { z } from "zod";
+
+import Button from "components/porter/Button";
+import Container from "components/porter/Container";
+import Icon from "components/porter/Icon";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { type ButtonStatus } from "main/home/app-dashboard/app-view/AppDataContainer";
+import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
+import { AppIcon, AppSource } from "main/home/app-dashboard/apps/AppMeta";
+import {
+  appRevisionWithSourceValidator,
+  type AppRevisionWithSource,
+} from "main/home/app-dashboard/apps/types";
+import { type PorterAppFormData } from "lib/porter-apps";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import healthy from "assets/status-healthy.png";
+
+type RowProps = {
+  idx: number;
+  app: AppRevisionWithSource;
+  append: UseFieldArrayAppend<PorterAppFormData, "app.requiredApps">;
+  remove: (index: number) => void;
+  selected?: boolean;
+};
+
+const RequiredAppRow: React.FC<RowProps> = ({
+  idx,
+  app,
+  selected,
+  append,
+  remove,
+}) => {
+  const proto = useMemo(() => {
+    return PorterApp.fromJsonString(atob(app.app_revision.b64_app_proto), {
+      ignoreUnknownFields: true,
+    });
+  }, [app.app_revision.b64_app_proto]);
+
+  return (
+    <ResourceOption
+      selected={selected}
+      onClick={() => {
+        if (selected) {
+          remove(idx);
+        } else {
+          append({ name: app.source.name });
+        }
+      }}
+    >
+      <div>
+        <Container row>
+          <Spacer inline width="1px" />
+          <AppIcon buildpacks={proto.build?.buildpacks ?? []} />
+          <Spacer inline width="12px" />
+          <Text size={14}>{proto.name}</Text>
+          <Spacer inline x={1} />
+        </Container>
+        <Spacer height="15px" />
+        <Container row>
+          <AppSource source={app.source} />
+          <Spacer inline x={1} />
+        </Container>
+      </div>
+      {selected && <Icon height="18px" src={healthy} />}
+    </ResourceOption>
+  );
+};
+
+type Props = {
+  buttonStatus: ButtonStatus;
+};
+
+export const RequiredApps: React.FC<Props> = ({ buttonStatus }) => {
+  const { currentCluster, currentProject } = useContext(Context);
+
+  const {
+    control,
+    formState: { isSubmitting },
+  } = useFormContext<PorterAppFormData>();
+  const { append, remove, fields } = useFieldArray({
+    control,
+    name: "app.requiredApps",
+  });
+
+  const { porterApp } = useLatestRevision();
+
+  const { data: apps = [] } = useQuery(
+    [
+      "getLatestAppRevisions",
+      {
+        cluster_id: currentCluster?.id,
+        project_id: currentProject?.id,
+      },
+    ],
+    async () => {
+      if (
+        !currentCluster ||
+        !currentProject ||
+        currentCluster.id === -1 ||
+        currentProject.id === -1
+      ) {
+        return;
+      }
+
+      const res = await api.getLatestAppRevisions(
+        "<token>",
+        {
+          deployment_target_id: undefined,
+          ignore_preview_apps: true,
+        },
+        { cluster_id: currentCluster.id, project_id: currentProject.id }
+      );
+
+      const apps = await z
+        .object({
+          app_revisions: z.array(appRevisionWithSourceValidator),
+        })
+        .parseAsync(res.data);
+
+      return apps.app_revisions;
+    },
+    {
+      refetchOnWindowFocus: false,
+      enabled: !!currentCluster && !!currentProject,
+    }
+  );
+
+  const remainingApps = useMemo(() => {
+    return apps.filter((a) => a.source.name !== porterApp.name);
+  }, [apps, porterApp, fields.length]);
+
+  return (
+    <div>
+      <Text size={16}>Required Apps</Text>
+      <Spacer y={0.5} />
+      <RequiredAppList>
+        {remainingApps.map((ra) => {
+          const selectedAppIdx = fields.findIndex(
+            (f) => f.name === ra.source.name
+          );
+
+          return (
+            <RequiredAppRow
+              idx={selectedAppIdx}
+              key={
+                selectedAppIdx !== -1
+                  ? fields[selectedAppIdx].id
+                  : ra.source.name
+              }
+              app={ra}
+              append={append}
+              remove={remove}
+              selected={selectedAppIdx !== -1}
+            />
+          );
+        })}
+      </RequiredAppList>
+      <Spacer y={0.75} />
+      <Button
+        type="submit"
+        status={buttonStatus}
+        loadingText={"Updating..."}
+        disabled={isSubmitting}
+      >
+        Update app
+      </Button>
+    </div>
+  );
+};
+
+const RequiredAppList = styled.div`
+  display: flex;
+  row-gap: 10px;
+  flex-direction: column;
+`;
+
+const ResourceOption = styled.div<{ selected?: boolean }>`
+  background: ${(props) => props.theme.clickable.bg};
+  border: 1px solid
+    ${(props) => (props.selected ? "#ffffff" : props.theme.border)};
+  width: 100%;
+  padding: 10px 15px;
+  border-radius: 5px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  cursor: pointer;
+  :hover {
+    border: 1px solid #ffffff;
+  }
+`;

+ 72 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/ServiceSettings.tsx

@@ -0,0 +1,72 @@
+import React from "react";
+import _ from "lodash";
+import { useFormContext } from "react-hook-form";
+
+import Button from "components/porter/Button";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { type ButtonStatus } from "main/home/app-dashboard/app-view/AppDataContainer";
+import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
+import ServiceList from "main/home/app-dashboard/validate-apply/services-settings/ServiceList";
+import { type PorterAppFormData } from "lib/porter-apps";
+import {
+  defaultSerialized,
+  deserializeService,
+} from "lib/porter-apps/services";
+
+import { useClusterResources } from "shared/ClusterResourcesContext";
+
+type Props = {
+  buttonStatus: ButtonStatus;
+};
+
+export const ServiceSettings: React.FC<Props> = ({ buttonStatus }) => {
+  const { deploymentTarget, porterApp, latestProto } = useLatestRevision();
+  const { currentClusterResources } = useClusterResources();
+
+  const {
+    formState: { isSubmitting },
+  } = useFormContext<PorterAppFormData>();
+
+  return (
+    <>
+      <Text size={16}>Pre-deploy job</Text>
+      <Spacer y={0.5} />
+      <ServiceList
+        addNewText={"Add a new pre-deploy job"}
+        prePopulateService={deserializeService({
+          service: defaultSerialized({
+            name: "pre-deploy",
+            type: "predeploy",
+            defaultCPU: currentClusterResources.defaultCPU,
+            defaultRAM: currentClusterResources.defaultRAM,
+          }),
+        })}
+        existingServiceNames={latestProto.predeploy ? ["pre-deploy"] : []}
+        isPredeploy
+        fieldArrayName={"app.predeploy"}
+      />
+      <Spacer y={0.5} />
+      <Text size={16}>Application services</Text>
+      <Spacer y={0.5} />
+      <ServiceList
+        addNewText={"Add a new service"}
+        fieldArrayName={"app.services"}
+        internalNetworkingDetails={{
+          namespace: deploymentTarget.namespace,
+          appName: porterApp.name,
+        }}
+        allowAddServices={false}
+      />
+      <Spacer y={0.75} />
+      <Button
+        type="submit"
+        status={buttonStatus}
+        loadingText={"Updating..."}
+        disabled={isSubmitting}
+      >
+        Update app
+      </Button>
+    </>
+  );
+};

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/SetupApp.tsx

@@ -16,7 +16,7 @@ import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import pull_request from "assets/pull_request_icon.svg";
 import pull_request from "assets/pull_request_icon.svg";
 
 
-import AppTemplateForm from "./AppTemplateForm";
+import { PreviewAppDataContainer } from "./PreviewAppDataContainer";
 
 
 type Props = RouteComponentProps;
 type Props = RouteComponentProps;
 
 
@@ -109,7 +109,7 @@ const SetupApp: React.FC<Props> = ({ location }) => {
             {match(templateRes)
             {match(templateRes)
               .with({ status: "loading" }, () => <Loading />)
               .with({ status: "loading" }, () => <Loading />)
               .with({ status: "success" }, ({ data }) => {
               .with({ status: "success" }, ({ data }) => {
-                return <AppTemplateForm existingTemplate={data} />;
+                return <PreviewAppDataContainer existingTemplate={data} />;
               })
               })
               .otherwise(() => null)}
               .otherwise(() => null)}
             <Spacer y={3} />
             <Spacer y={3} />

+ 9 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/types.ts

@@ -0,0 +1,9 @@
+import { type PorterApp } from "@porter-dev/api-contracts";
+
+export type ExistingTemplateWithEnv = {
+  template: PorterApp;
+  env: {
+    variables: Record<string, string>;
+    secret_variables: Record<string, string>;
+  };
+};

+ 175 - 0
dashboard/src/main/home/managed-addons/AddonListRow.tsx

@@ -0,0 +1,175 @@
+import React from "react";
+import { AnimatePresence, motion } from "framer-motion";
+import { type UseFieldArrayUpdate } from "react-hook-form";
+import styled from "styled-components";
+import { match } from "ts-pattern";
+
+import { type ClientAddon } from "lib/addons";
+
+import postgresql from "assets/postgresql.svg";
+
+import { type AppTemplateFormData } from "../cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer";
+import { PostgresTabs } from "./tabs/PostgresTabs";
+
+type AddonRowProps = {
+  index: number;
+  addon: ClientAddon;
+  update: UseFieldArrayUpdate<AppTemplateFormData, "addons">;
+  remove: (index: number) => void;
+};
+
+export const AddonListRow: React.FC<AddonRowProps> = ({
+  index,
+  addon,
+  update,
+  remove,
+}) => {
+  const renderIcon = (): JSX.Element => <Icon src={postgresql} />;
+
+  return (
+    <>
+      <AddonHeader
+        showExpanded={addon.expanded}
+        onClick={() => {
+          update(index, {
+            ...addon,
+            expanded: !addon.expanded,
+          });
+        }}
+        bordersRounded={!addon.expanded}
+      >
+        <AddonTitle>
+          <ActionButton>
+            <span className="material-icons dropdown">arrow_drop_down</span>
+          </ActionButton>
+          {renderIcon()}
+          {addon.name.value.trim().length > 0 ? addon.name.value : "New Addon"}
+        </AddonTitle>
+
+        {addon.canDelete && (
+          <ActionButton
+            onClick={(e) => {
+              e.stopPropagation();
+              remove(index);
+            }}
+          >
+            <span className="material-icons">delete</span>
+          </ActionButton>
+        )}
+      </AddonHeader>
+      <AnimatePresence>
+        {addon.expanded && (
+          <StyledSourceBox
+            key={addon.name.value}
+            initial={{
+              height: 0,
+            }}
+            animate={{
+              height: "auto",
+              transition: {
+                duration: 0.3,
+              },
+            }}
+            exit={{
+              height: 0,
+              transition: {
+                duration: 0.3,
+              },
+            }}
+            showExpanded={addon.expanded}
+          >
+            <div
+              style={{
+                padding: "14px 25px 30px",
+                border: "1px solid #494b4f",
+              }}
+            >
+              {match(addon.config.type)
+                .with("postgres", () => (
+                  <PostgresTabs index={index} addon={addon} />
+                ))
+                .exhaustive()}
+            </div>
+          </StyledSourceBox>
+        )}
+      </AnimatePresence>
+    </>
+  );
+};
+
+const AddonTitle = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const AddonHeader = styled.div<{
+  showExpanded?: boolean;
+  bordersRounded?: boolean;
+}>`
+  flex-direction: row;
+  display: flex;
+  height: 60px;
+  justify-content: space-between;
+  cursor: pointer;
+  padding: 20px;
+  color: ${(props) => props.theme.text.primary};
+  position: relative;
+  border-radius: 5px;
+  background: ${(props) => props.theme.clickable.bg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+
+  border-bottom-left-radius: ${(props) => (props.bordersRounded ? "" : "0")};
+  border-bottom-right-radius: ${(props) => (props.bordersRounded ? "" : "0")};
+
+  .dropdown {
+    font-size: 30px;
+    cursor: pointer;
+    border-radius: 20px;
+    margin-left: -10px;
+    transform: ${(props: { showExpanded?: boolean }) =>
+      props.showExpanded ? "" : "rotate(-90deg)"};
+  }
+`;
+
+const Icon = styled.img`
+  height: 18px;
+  margin-right: 15px;
+`;
+
+const ActionButton = styled.button`
+  position: relative;
+  border: none;
+  background: none;
+  color: white;
+  padding: 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  cursor: pointer;
+  color: #aaaabb;
+  :hover {
+    color: white;
+  }
+
+  > span {
+    font-size: 20px;
+  }
+  margin-right: 5px;
+`;
+
+const StyledSourceBox = styled(motion.div)<{
+  showExpanded?: boolean;
+  hasFooter?: boolean;
+}>`
+  overflow: hidden;
+  color: #ffffff;
+  font-size: 13px;
+  background: ${(props) => props.theme.fg};
+  border-top: 0;
+  border-bottom-left-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
+  border-bottom-right-radius: ${(props) => (props.hasFooter ? "0" : "5px")};
+`;

+ 246 - 0
dashboard/src/main/home/managed-addons/AddonsList.tsx

@@ -0,0 +1,246 @@
+import React, { useEffect, useState } from "react";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Controller,
+  useFieldArray,
+  useForm,
+  useFormContext,
+} from "react-hook-form";
+import styled from "styled-components";
+import { match } from "ts-pattern";
+import { z } from "zod";
+
+import Button from "components/porter/Button";
+import Container from "components/porter/Container";
+import { ControlledInput } from "components/porter/ControlledInput";
+import Modal from "components/porter/Modal";
+import Select from "components/porter/Select";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { type AppTemplateFormData } from "main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer";
+import { defaultClientAddon } from "lib/addons";
+
+import postgresql from "assets/postgresql.svg";
+
+import { AddonListRow } from "./AddonListRow";
+
+const addAddonFormValidator = z.object({
+  name: z
+    .string()
+    .min(1, { message: "A service name is required" })
+    .max(30)
+    .regex(/^[a-z0-9-]+$/, {
+      message: 'Lowercase letters, numbers, and " - " only.',
+    }),
+  type: z.enum(["postgres"]),
+});
+type AddAddonFormValues = z.infer<typeof addAddonFormValidator>;
+
+export const AddonsList: React.FC = () => {
+  const [showAddAddonModal, setShowAddAddonModal] = useState(false);
+
+  const { control: appTemplateControl } = useFormContext<AppTemplateFormData>();
+
+  // add addon modal form
+  const {
+    register,
+    watch,
+    control,
+    reset,
+    handleSubmit,
+    formState: { errors },
+    setError,
+    clearErrors,
+  } = useForm<AddAddonFormValues>({
+    reValidateMode: "onChange",
+    resolver: zodResolver(addAddonFormValidator),
+    defaultValues: {
+      name: "",
+      type: "postgres",
+    },
+  });
+
+  const addonName = watch("name");
+  const addonType = watch("type");
+
+  const { append, update, remove, fields } = useFieldArray({
+    control: appTemplateControl,
+    name: "addons",
+  });
+
+  useEffect(() => {
+    const existingAddonNames = fields.map((f) => f.name);
+    if (existingAddonNames.some((n) => n.value === addonName)) {
+      setError("name", {
+        message: "Addon name must be unique",
+      });
+    } else {
+      clearErrors("name");
+    }
+  }, [fields]);
+
+  const onSubmit = handleSubmit((data) => {
+    const baseAddon = defaultClientAddon();
+    append({
+      ...baseAddon,
+      name: {
+        value: data.name,
+        readOnly: false,
+      },
+    });
+
+    reset();
+    setShowAddAddonModal(false);
+  });
+
+  return (
+    <>
+      <AddonsContainer>
+        {fields.map((addon, idx) => (
+          <AddonListRow
+            key={addon.id}
+            index={idx}
+            addon={addon}
+            update={update}
+            remove={remove}
+          />
+        ))}
+      </AddonsContainer>
+      {fields.length === 0 && (
+        <>
+          <AddAddonButton
+            onClick={() => {
+              setShowAddAddonModal(true);
+            }}
+          >
+            <I className="material-icons add-icon">add</I>
+            Include add-on in preview environments
+          </AddAddonButton>
+          <Spacer y={0.5} />
+        </>
+      )}
+      {showAddAddonModal && (
+        <Modal
+          closeModal={() => {
+            setShowAddAddonModal(false);
+          }}
+          width="500px"
+        >
+          <Text size={16}>Include an addon in your preview environment</Text>
+          <Spacer y={1} />
+          <Text color="helper">Select a service type:</Text>
+          <Spacer y={0.5} />
+          <Container row>
+            <AddonIcon>
+              {match(addonType)
+                .with("postgres", () => <img src={postgresql} />)
+                .exhaustive()}
+            </AddonIcon>
+            <Controller
+              name="type"
+              control={control}
+              render={({ field: { onChange } }) => (
+                <Select
+                  value={addonType}
+                  width="100%"
+                  setValue={(value: string) => {
+                    onChange(value);
+                  }}
+                  options={[{ label: "Postgres", value: "postgres" }]}
+                />
+              )}
+            />
+          </Container>
+          <Spacer y={1} />
+          <Text color="helper">Name this service:</Text>
+          <Spacer y={0.5} />
+          <ControlledInput
+            type="text"
+            placeholder="ex: my-postgres"
+            width="100%"
+            error={errors.name?.message}
+            {...register("name")}
+          />
+          <Spacer y={1} />
+          <Button
+            type="button"
+            onClick={onSubmit}
+            disabled={!!errors.name?.message}
+          >
+            <I className="material-icons">add</I> Add service
+          </Button>
+        </Modal>
+      )}
+    </>
+  );
+};
+
+const AddonsContainer = styled.div`
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const AddonIcon = styled.div`
+  border: 1px solid #494b4f;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 35px;
+  width: 35px;
+  min-width: 35px;
+  margin-right: 10px;
+  overflow: hidden;
+  border-radius: 5px;
+  > img {
+    height: 18px;
+    animation: floatIn 0.5s 0s;
+    @keyframes floatIn {
+      from {
+        opacity: 0;
+        transform: translateY(7px);
+      }
+      to {
+        opacity: 1;
+        transform: translateY(0px);
+      }
+    }
+  }
+`;
+
+const AddAddonButton = styled.div`
+  color: #aaaabb;
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+    color: white;
+  }
+  display: flex;
+  align-items: center;
+  border-radius: 5px;
+  height: 40px;
+  font-size: 13px;
+  width: 100%;
+  padding-left: 10px;
+  cursor: pointer;
+  .add-icon {
+    width: 30px;
+    font-size: 20px;
+  }
+`;
+
+const I = styled.i`
+  color: white;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  margin-right: 7px;
+  justify-content: center;
+`;

+ 191 - 0
dashboard/src/main/home/managed-addons/tabs/PostgresTabs.tsx

@@ -0,0 +1,191 @@
+import React, { useMemo, useState } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import styled from "styled-components";
+import { match } from "ts-pattern";
+
+import CopyToClipboard from "components/CopyToClipboard";
+import { ControlledInput } from "components/porter/ControlledInput";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import TabSelector from "components/TabSelector";
+import IntelligentSlider from "main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider";
+import { type AppTemplateFormData } from "main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer";
+import { type ClientAddon } from "lib/addons";
+
+import { useClusterResources } from "shared/ClusterResourcesContext";
+import copy from "assets/copy-left.svg";
+
+type Props = {
+  index: number;
+  addon: ClientAddon & {
+    config: {
+      type: "postgres";
+    };
+  };
+};
+
+export const PostgresTabs: React.FC<Props> = ({ index }) => {
+  const { register, control, watch } = useFormContext<AppTemplateFormData>();
+  const {
+    currentClusterResources: { maxCPU, maxRAM },
+  } = useClusterResources();
+
+  const [currentTab, setCurrentTab] = useState<"credentials" | "resources">(
+    "credentials"
+  );
+
+  const name = watch(`addons.${index}.name`);
+  const username = watch(`addons.${index}.config.username`);
+  const password = watch(`addons.${index}.config.password`);
+
+  const databaseURL = useMemo(() => {
+    if (!username || !password || !name.value) {
+      return "";
+    }
+
+    return `postgresql://${username}:${password}@${name.value}-postgres:5432/postgres`;
+  }, [username, password, name.value]);
+
+  return (
+    <>
+      <TabSelector
+        options={[
+          { label: "Credentials", value: "credentials" },
+          { label: "Resources", value: "resources" },
+        ]}
+        currentTab={currentTab}
+        setCurrentTab={setCurrentTab}
+      />
+      <Spacer y={1} />
+      {match(currentTab)
+        .with("credentials", () => (
+          <>
+            <Text color="helper">Postgres Username</Text>
+            <Spacer y={0.25} />
+            <ControlledInput
+              type="text"
+              placeholder="postgres"
+              width="300px"
+              {...register(`addons.${index}.config.username`)}
+            />
+            <Spacer y={1} />
+            <Text color="helper">Postgres Password</Text>
+            <Spacer y={0.25} />
+            <ControlledInput
+              type="text"
+              width="300px"
+              {...register(`addons.${index}.config.password`)}
+            />
+            <Spacer y={1} />
+            {databaseURL && (
+              <>
+                <Text color="helper">Internal Database URL:</Text>
+                <Spacer y={0.5} />
+                <IdContainer>
+                  <Code>{databaseURL}</Code>
+                  <CopyContainer>
+                    <CopyToClipboard text={databaseURL}>
+                      <CopyIcon src={copy} alt="copy" />
+                    </CopyToClipboard>
+                  </CopyContainer>
+                </IdContainer>
+                <Spacer y={0.5} />
+              </>
+            )}
+          </>
+        ))
+        .with("resources", () => (
+          <>
+            <Controller
+              name={`addons.${index}.config.cpuCores`}
+              control={control}
+              render={({ field: { value, onChange } }) => (
+                <IntelligentSlider
+                  label="CPUs: "
+                  unit="Cores"
+                  min={0.01}
+                  max={maxCPU}
+                  color={"#3f51b5"}
+                  value={value.value.toString()}
+                  setValue={(e) => {
+                    onChange({
+                      ...value,
+                      value: e,
+                    });
+                  }}
+                  step={0.1}
+                  disabled={value.readOnly}
+                  disabledTooltip={
+                    "You may only edit this field in your porter.yaml."
+                  }
+                  isSmartOptimizationOn={false}
+                  decimalsToRoundTo={2}
+                />
+              )}
+            />
+            <Spacer y={1} />
+            <Controller
+              name={`addons.${index}.config.ramMegabytes`}
+              control={control}
+              render={({ field: { value, onChange } }) => (
+                <IntelligentSlider
+                  label="RAM: "
+                  unit="MB"
+                  min={1}
+                  max={maxRAM}
+                  color={"#3f51b5"}
+                  value={value.value.toString()}
+                  setValue={(e) => {
+                    onChange({
+                      ...value,
+                      value: e,
+                    });
+                  }}
+                  step={10}
+                  disabled={value.readOnly}
+                  disabledTooltip={
+                    "You may only edit this field in your porter.yaml."
+                  }
+                  isSmartOptimizationOn={false}
+                />
+              )}
+            />
+          </>
+        ))
+        .exhaustive()}
+    </>
+  );
+};
+
+const CopyIcon = styled.img`
+  cursor: pointer;
+  margin-left: 5px;
+  margin-right: 5px;
+  width: 15px;
+  height: 15px;
+  :hover {
+    opacity: 0.8;
+  }
+`;
+
+const Code = styled.span`
+  font-family: monospace;
+`;
+
+const IdContainer = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  padding: 10px;
+  display: flex;
+  width: 550px;
+  border-radius: 5px;
+  border: 1px solid ${({ theme }) => theme.border};
+  align-items: center;
+  user-select: text;
+`;
+
+const CopyContainer = styled.div`
+  display: flex;
+  align-items: center;
+  margin-left: auto;
+`;

+ 5 - 0
dashboard/src/shared/api.tsx

@@ -981,6 +981,11 @@ const createAppTemplate = baseApi<
     variables: Record<string, string>;
     variables: Record<string, string>;
     secrets: Record<string, string>;
     secrets: Record<string, string>;
     base_deployment_target_id: string;
     base_deployment_target_id: string;
+    addons?: Array<{
+      base64_addon: string;
+      variables: Record<string, string>;
+      secrets: Record<string, string>;
+    }>
   },
   },
   {
   {
     project_id: number;
     project_id: number;

+ 1 - 0
dashboard/src/shared/types.tsx

@@ -293,6 +293,7 @@ export type ProjectType = {
   stacks_enabled: boolean;
   stacks_enabled: boolean;
   validate_apply_v2: boolean;
   validate_apply_v2: boolean;
   managed_deployment_targets_enabled: boolean;
   managed_deployment_targets_enabled: boolean;
+  aws_ack_auth_enabled: boolean;
   roles: Array<{
   roles: Array<{
     id: number;
     id: number;
     kind: string;
     kind: string;

+ 7 - 2
dashboard/webpack.config.js

@@ -5,8 +5,8 @@ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"
 
 
 const dotenv = require("dotenv");
 const dotenv = require("dotenv");
 
 
-const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
-  .BundleAnalyzerPlugin;
+const BundleAnalyzerPlugin =
+  require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
 
 
 const TerserPlugin = require("terser-webpack-plugin");
 const TerserPlugin = require("terser-webpack-plugin");
 
 
@@ -87,6 +87,11 @@ module.exports = () => {
             },
             },
           ],
           ],
         },
         },
+        {
+          test: /\.mjs/,
+          include: /node_modules/,
+          type: "javascript/auto",
+        },
         {
         {
           enforce: "pre",
           enforce: "pre",
           test: /\.js$/,
           test: /\.js$/,

+ 1 - 1
go.mod

@@ -83,7 +83,7 @@ require (
 	github.com/matryer/is v1.4.0
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.2.68
+	github.com/porter-dev/api-contracts v0.2.73
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d

+ 2 - 2
go.sum

@@ -1520,8 +1520,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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 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/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.2.68 h1:OeU3RQAI6IpGC99UdDalrlRnNn7nevoxjm+Gm6n8PEY=
-github.com/porter-dev/api-contracts v0.2.68/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.2.73 h1:hsFcJSf0HLxS7VgV36qn5X3tYPzWG48mCvHwuOlU2eE=
+github.com/porter-dev/api-contracts v0.2.73/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 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/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 357 - 0
go.work.sum

@@ -843,9 +843,24 @@ github.com/porter-dev/api-contracts v0.2.55 h1:H8RvD004mX4uWrlRVcL8kzo7ZtFQyZDN+
 github.com/porter-dev/api-contracts v0.2.55/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/api-contracts v0.2.55/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/api-contracts v0.2.68 h1:OeU3RQAI6IpGC99UdDalrlRnNn7nevoxjm+Gm6n8PEY=
 github.com/porter-dev/api-contracts v0.2.68 h1:OeU3RQAI6IpGC99UdDalrlRnNn7nevoxjm+Gm6n8PEY=
 github.com/porter-dev/api-contracts v0.2.68/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/api-contracts v0.2.68/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.2.73 h1:hsFcJSf0HLxS7VgV36qn5X3tYPzWG48mCvHwuOlU2eE=
+github.com/porter-dev/api-contracts v0.2.73/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.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
 github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
 github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
 github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
 github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
 github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
+github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=
 github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=
 github.com/pseudomuto/protoc-gen-doc v1.3.2 h1:61vWZuxYa8D7Rn4h+2dgoTNqnluBmJya2MgbqO32z6g=
 github.com/pseudomuto/protoc-gen-doc v1.3.2 h1:61vWZuxYa8D7Rn4h+2dgoTNqnluBmJya2MgbqO32z6g=
 github.com/pseudomuto/protokit v0.2.0 h1:hlnBDcy3YEDXH7kc9gV+NLaN0cDzhDvD1s7Y6FZ8RpM=
 github.com/pseudomuto/protokit v0.2.0 h1:hlnBDcy3YEDXH7kc9gV+NLaN0cDzhDvD1s7Y6FZ8RpM=
@@ -854,59 +869,140 @@ github.com/quasilyte/go-ruleguard v0.3.13 h1:O1G41cq1jUr3cJmqp7vOUT0SokqjzmS9aES
 github.com/quasilyte/go-ruleguard/dsl v0.3.10 h1:4tVlVVcBT+nNWoF+t/zrAMO13sHAqYotX1K12Gc8f8A=
 github.com/quasilyte/go-ruleguard/dsl v0.3.10 h1:4tVlVVcBT+nNWoF+t/zrAMO13sHAqYotX1K12Gc8f8A=
 github.com/quasilyte/go-ruleguard/rules v0.0.0-20210428214800-545e0d2e0bf7 h1:cRLFDAB53r5wIkxYvtQUMnn3+B09uZTAOPmefNfVk5I=
 github.com/quasilyte/go-ruleguard/rules v0.0.0-20210428214800-545e0d2e0bf7 h1:cRLFDAB53r5wIkxYvtQUMnn3+B09uZTAOPmefNfVk5I=
 github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=
 github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 h1:L8QM9bvf68pVdQ3bCFZMDmnt9yqcMBro1pC7F+IPYMY=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/riandyrn/otelchi v0.5.1 h1:0/45omeqpP7f/cvdL16GddQBfAEmZvUyl2QzLSE6uYo=
+github.com/riandyrn/otelchi v0.5.1/go.mod h1:ZxVxNEl+jQ9uHseRYIxKWRb3OY8YXFEu+EkNiiSNUEA=
+github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc=
+github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
 github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
 github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
 github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
 github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
+github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE=
+github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/ryancurrah/gomodguard v1.2.3 h1:ww2fsjqocGCAFamzvv/b8IsRduuHHeK2MHTcTxZTQX8=
 github.com/ryancurrah/gomodguard v1.2.3 h1:ww2fsjqocGCAFamzvv/b8IsRduuHHeK2MHTcTxZTQX8=
 github.com/ryanrolds/sqlclosecheck v0.3.0 h1:AZx+Bixh8zdUBxUA1NxbxVAS78vTPq4rCb8OUZI9xFw=
 github.com/ryanrolds/sqlclosecheck v0.3.0 h1:AZx+Bixh8zdUBxUA1NxbxVAS78vTPq4rCb8OUZI9xFw=
 github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s=
 github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s=
+github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
+github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
 github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 h1:2c1EFnZHIPCW8qKWgHMH/fX2PkSabFc5mrVzfUNdg5U=
 github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 h1:2c1EFnZHIPCW8qKWgHMH/fX2PkSabFc5mrVzfUNdg5U=
+github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
 github.com/sagikazarmark/crypt v0.3.0 h1:TV5DVog+pihN4Rr0rN1IClv4ePpkzdg9sPrw7WDofZ8=
 github.com/sagikazarmark/crypt v0.3.0 h1:TV5DVog+pihN4Rr0rN1IClv4ePpkzdg9sPrw7WDofZ8=
 github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
 github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
 github.com/sanposhiho/wastedassign/v2 v2.0.6 h1:+6/hQIHKNJAUixEj6EmOngGIisyeI+T3335lYTyxRoA=
 github.com/sanposhiho/wastedassign/v2 v2.0.6 h1:+6/hQIHKNJAUixEj6EmOngGIisyeI+T3335lYTyxRoA=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 h1:HNLA3HtUIROrQwG1cuu5EYuqk3UEoJ61Dr/9xkd6sok=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.1/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
 github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
 github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
 github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
 github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
+github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
+github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
 github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
 github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
+github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
 github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 h1:58EBmR2dMNL2n/FnbQewK3D14nXr0V9CObDSvMJLq+Y=
 github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921 h1:58EBmR2dMNL2n/FnbQewK3D14nXr0V9CObDSvMJLq+Y=
+github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
 github.com/securego/gosec/v2 v2.9.1 h1:anHKLS/ApTYU6NZkKa/5cQqqcbKZURjvc+MtR++S4EQ=
 github.com/securego/gosec/v2 v2.9.1 h1:anHKLS/ApTYU6NZkKa/5cQqqcbKZURjvc+MtR++S4EQ=
+github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 h1:ZuhckGJ10ulaKkdvJtiAqsLTiPrLaXSdnVgXJKJkTxE=
+github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
+github.com/sendgrid/rest v2.6.3+incompatible h1:h/uruXAzKxVyDDIQX/MkQI73p/gsdpEnb5q2wxSvTsA=
+github.com/sendgrid/rest v2.6.3+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
+github.com/sendgrid/sendgrid-go v3.8.0+incompatible h1:7yoUFMwT+jDI2ArBpC6zvtuQj1RUyYfCDl7zZea3XV4=
+github.com/sendgrid/sendgrid-go v3.8.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 h1:ka9QPuQg2u4LGipiZGsgkg3rJCo4iIUCy75FddM0GRQ=
 github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 h1:ka9QPuQg2u4LGipiZGsgkg3rJCo4iIUCy75FddM0GRQ=
 github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
 github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
+github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE=
+github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0=
 github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
 github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
+github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
+github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
+github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
+github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
 github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
 github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
 github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b h1:0/ecDXh/HTHRtSDSFnD2/Ta1yQ5J76ZspVY4u0/jGFk=
 github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b h1:0/ecDXh/HTHRtSDSFnD2/Ta1yQ5J76ZspVY4u0/jGFk=
+github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
 github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
 github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
 github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
 github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
 github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
 github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
+github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
+github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sivchari/tenv v1.4.7 h1:FdTpgRlTue5eb5nXIYgS/lyVXSjugU8UUVDwhP1NLU8=
 github.com/sivchari/tenv v1.4.7 h1:FdTpgRlTue5eb5nXIYgS/lyVXSjugU8UUVDwhP1NLU8=
 github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
 github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
+github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
 github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q=
 github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q=
+github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 github.com/smartystreets/gunit v1.0.0 h1:RyPDUFcJbvtXlhJPk7v+wnxZRY2EUokhEYl2EJOPToI=
 github.com/smartystreets/gunit v1.0.0 h1:RyPDUFcJbvtXlhJPk7v+wnxZRY2EUokhEYl2EJOPToI=
+github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
 github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
 github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
 github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
 github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
 github.com/sonatard/noctx v0.0.1 h1:VC1Qhl6Oxx9vvWo3UDgrGXYCeKCe3Wbw7qAWL6FrmTY=
 github.com/sonatard/noctx v0.0.1 h1:VC1Qhl6Oxx9vvWo3UDgrGXYCeKCe3Wbw7qAWL6FrmTY=
 github.com/sourcegraph/go-diff v0.6.1 h1:hmA1LzxW0n1c3Q4YbrFgg4P99GSnebYa3x8gr0HZqLQ=
 github.com/sourcegraph/go-diff v0.6.1 h1:hmA1LzxW0n1c3Q4YbrFgg4P99GSnebYa3x8gr0HZqLQ=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
 github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
 github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
+github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
+github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.10.0 h1:mXH0UwHS4D2HwWZa75im4xIQynLfblmWV7qcWpfv0yk=
+github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
+github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
+github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
 github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
 github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
 github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I=
 github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
 github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
 github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
+github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/sylvia7788/contextcheck v1.0.4 h1:MsiVqROAdr0efZc/fOCt0c235qm9XJqHtWwM+2h2B04=
 github.com/sylvia7788/contextcheck v1.0.4 h1:MsiVqROAdr0efZc/fOCt0c235qm9XJqHtWwM+2h2B04=
+github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
 github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck=
 github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck=
+github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
+github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
+github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
 github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b h1:HxLVTlqcHhFAz3nWUcuvpH7WuOMv8LQoCWmruLfFH2U=
 github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b h1:HxLVTlqcHhFAz3nWUcuvpH7WuOMv8LQoCWmruLfFH2U=
 github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
 github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
 github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
 github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
 github.com/tetafro/godot v1.4.11 h1:BVoBIqAf/2QdbFmSwAWnaIqDivZdOV0ZRwEm6jivLKw=
 github.com/tetafro/godot v1.4.11 h1:BVoBIqAf/2QdbFmSwAWnaIqDivZdOV0ZRwEm6jivLKw=
 github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 h1:ig99OeTyDwQWhPe2iw9lwfQVF1KB3Q4fpP3X7/2VBG8=
 github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 h1:ig99OeTyDwQWhPe2iw9lwfQVF1KB3Q4fpP3X7/2VBG8=
+github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
+github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
 github.com/tj/go-buffer v1.1.0 h1:Lo2OsPHlIxXF24zApe15AbK3bJLAOvkkxEA6Ux4c47M=
 github.com/tj/go-buffer v1.1.0 h1:Lo2OsPHlIxXF24zApe15AbK3bJLAOvkkxEA6Ux4c47M=
+github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
 github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2 h1:eGaGNxrtoZf/mBURsnNQKDR7u50Klgcf2eFDQEnc8Bc=
 github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2 h1:eGaGNxrtoZf/mBURsnNQKDR7u50Klgcf2eFDQEnc8Bc=
+github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
 github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b h1:m74UWYy+HBs+jMFR9mdZU6shPewugMyH5+GV6LNgW8w=
 github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b h1:m74UWYy+HBs+jMFR9mdZU6shPewugMyH5+GV6LNgW8w=
+github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
 github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
 github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
+github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
+github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
+github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
+github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tomarrell/wrapcheck/v2 v2.4.0 h1:mU4H9KsqqPZUALOUbVOpjy8qNQbWLoLI9fV68/1tq30=
 github.com/tomarrell/wrapcheck/v2 v2.4.0 h1:mU4H9KsqqPZUALOUbVOpjy8qNQbWLoLI9fV68/1tq30=
@@ -922,41 +1018,83 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/
 github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
 github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
 github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f h1:DLpt6B5oaaS8jyXHa9VA4rrZloBVPVXeCtrOsrFauxc=
 github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f h1:DLpt6B5oaaS8jyXHa9VA4rrZloBVPVXeCtrOsrFauxc=
 github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
 github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
 github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
 github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
 github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
 github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
 github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
+github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
 github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
+github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
 github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
+github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
 github.com/uudashr/gocognit v1.0.5 h1:rrSex7oHr3/pPLQ0xoWq108XMU8s678FJcQ+aSfOHa4=
 github.com/uudashr/gocognit v1.0.5 h1:rrSex7oHr3/pPLQ0xoWq108XMU8s678FJcQ+aSfOHa4=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
 github.com/valyala/fasthttp v1.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g=
 github.com/valyala/fasthttp v1.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g=
 github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
 github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
+github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
 github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
 github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
 github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
+github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
 github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 h1:EVObHAr8DqpoJCVv6KYTle8FEImKhtkfcZetNqxDoJQ=
 github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 h1:EVObHAr8DqpoJCVv6KYTle8FEImKhtkfcZetNqxDoJQ=
+github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
 github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 h1:+UB2BJA852UkGH42H+Oee69djmxS3ANzl2b/JtT1YiA=
 github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 h1:+UB2BJA852UkGH42H+Oee69djmxS3ANzl2b/JtT1YiA=
 github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
 github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
+github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
 github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
 github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
 github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
 github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
 github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
+github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
+github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
+github.com/xanzy/go-gitlab v0.68.0 h1:b2iMQHgZ1V+NyRqLRJVv6RFfr4xnd/AASeS/PETYL0Y=
+github.com/xanzy/go-gitlab v0.68.0/go.mod h1:o4yExCtdaqlM8YGdDJWuZoBmfxBsmA9TPEjs9mx1UO4=
+github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
+github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
+github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
+github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
 github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
 github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow=
+github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
+github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
+github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
+github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg=
+github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
 github.com/yeya24/promlinter v0.1.0 h1:goWULN0jH5Yajmu/K+v1xCqIREeB+48OiJ2uu2ssc7U=
 github.com/yeya24/promlinter v0.1.0 h1:goWULN0jH5Yajmu/K+v1xCqIREeB+48OiJ2uu2ssc7U=
 github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
 github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
 github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
+github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
 github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
 github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
 github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
+github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
+github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
+github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
+github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
 github.com/zclconf/go-cty v1.9.1 h1:viqrgQwFl5UpSxc046qblj78wZXVDFnSOufaOTER+cc=
 github.com/zclconf/go-cty v1.9.1 h1:viqrgQwFl5UpSxc046qblj78wZXVDFnSOufaOTER+cc=
 github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
 github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
 github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
 github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
 github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
 github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
 github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ=
 github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ=
+go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
 go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
+go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
 go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo=
 go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo=
+go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
 go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc=
 go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc=
 go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
 go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
 go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg=
 go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg=
@@ -973,105 +1111,318 @@ go.etcd.io/etcd/server/v3 v3.5.4 h1:CMAZd0g8Bn5NRhynW6pKhc4FRg41/0QYy3d7aNm9874=
 go.etcd.io/etcd/server/v3 v3.5.4/go.mod h1:S5/YTU15KxymM5l3T6b09sNOHPXqGYIZStpuuGbb65c=
 go.etcd.io/etcd/server/v3 v3.5.4/go.mod h1:S5/YTU15KxymM5l3T6b09sNOHPXqGYIZStpuuGbb65c=
 go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403 h1:rKyWXYDfrVOpMFBion4Pmx5sJbQreQNXycHvm4KwJSg=
 go.mozilla.org/mozlog v0.0.0-20170222151521-4bb13139d403 h1:rKyWXYDfrVOpMFBion4Pmx5sJbQreQNXycHvm4KwJSg=
 go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
 go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
+go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
 go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
+go.opentelemetry.io/contrib v1.0.0 h1:khwDCxdSspjOLmFnvMuSHd/5rPzbTx0+l6aURwtQdfE=
+go.opentelemetry.io/contrib v1.0.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0 h1:n9b7AAdbQtQ0k9dm0Dm2/KUcUqtG8i2O15KzNaDze8c=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0 h1:n9b7AAdbQtQ0k9dm0Dm2/KUcUqtG8i2O15KzNaDze8c=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0/go.mod h1:LsankqVDx4W+RhZNA5uWarULII/MBhF5qwCYxTuyXjs=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0/go.mod h1:LsankqVDx4W+RhZNA5uWarULII/MBhF5qwCYxTuyXjs=
+go.opentelemetry.io/contrib/instrumentation/host v0.42.0 h1:/GMlvboQJd4LWxNX/oGYLv06J5a/M/flauLruM/3U2g=
+go.opentelemetry.io/contrib/instrumentation/host v0.42.0/go.mod h1:w6v1mVemRjTTdfejACjf+LgVA6zKtHOWmdAIf3icx7A=
 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0 h1:Wjp9vsVSIEyvdiaECfqxY9xBqQ7JaSCGtvHgR4doXZk=
 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0 h1:Wjp9vsVSIEyvdiaECfqxY9xBqQ7JaSCGtvHgR4doXZk=
 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0/go.mod h1:vHItvsnJtp7ES++nFLLFBzUWny7fJQSvTlxFcqQGUr4=
 go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0/go.mod h1:vHItvsnJtp7ES++nFLLFBzUWny7fJQSvTlxFcqQGUr4=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.32.0 h1:mac9BKRqwaX6zxHPDe3pvmWpwuuIM0vuXv2juCnQevE=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.32.0 h1:mac9BKRqwaX6zxHPDe3pvmWpwuuIM0vuXv2juCnQevE=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.32.0/go.mod h1:5eCOqeGphOyz6TsY3ZDNjE33SM/TFAK3RGuCL2naTgY=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.32.0/go.mod h1:5eCOqeGphOyz6TsY3ZDNjE33SM/TFAK3RGuCL2naTgY=
+go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 h1:EbmAUG9hEAMXyfWEasIt2kmh/WmXUznUksChApTgBGc=
+go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0/go.mod h1:rD9feqRYP24P14t5kmhNMqsqm1jvKmpx2H2rKVw52V8=
+go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo=
+go.opentelemetry.io/contrib/propagators/b3 v1.17.0/go.mod h1:IkfUfMpKWmynvvE0264trz0sf32NRTZL4nuAN9AbWRc=
+go.opentelemetry.io/contrib/propagators/ot v1.17.0 h1:ufo2Vsz8l76eI47jFjuVyjyB3Ae2DmfiCV/o6Vc8ii0=
+go.opentelemetry.io/contrib/propagators/ot v1.17.0/go.mod h1:SbKPj5XGp8K/sGm05XblaIABgMgw2jDczP8gGeuaVLk=
+go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
+go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
+go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
 go.opentelemetry.io/otel/exporters/jaeger v1.4.1 h1:VHCK+2yTZDqDaVXj7JH2Z/khptuydo6C0ttBh2bxAbc=
 go.opentelemetry.io/otel/exporters/jaeger v1.4.1 h1:VHCK+2yTZDqDaVXj7JH2Z/khptuydo6C0ttBh2bxAbc=
 go.opentelemetry.io/otel/exporters/jaeger v1.4.1/go.mod h1:ZW7vkOu9nC1CxsD8bHNHCia5JUbwP39vxgd1q4Z5rCI=
 go.opentelemetry.io/otel/exporters/jaeger v1.4.1/go.mod h1:ZW7vkOu9nC1CxsD8bHNHCia5JUbwP39vxgd1q4Z5rCI=
 go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg=
 go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg=
+go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0=
+go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 h1:f6BwB2OACc3FCbYVznctQ9V6KK7Vq6CjmYXJ7DeSs4E=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0/go.mod h1:UqL5mZ3qs6XYhDnZaW1Ps4upD+PX6LipH40AoeuIlwU=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0 h1:rm+Fizi7lTM2UefJ1TO347fSRcwmIsUAaZmYmIGBRAo=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0/go.mod h1:sWFbI3jJ+6JdjOVepA5blpv/TJ20Hw+26561iMbWcwU=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.39.0 h1:IZXpCEtI7BbX01DRQEWTGDkvjMB6hEhiEZXS+eg2YqY=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.39.0/go.mod h1:xY111jIZtWb+pUUgT4UiiSonAaY2cD2Ts5zvuKLki3o=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 h1:iqjq9LAB8aK++sKVcELezzn655JnBNdsDhghU4G/So8=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0/go.mod h1:hGXzO5bhhSHZnKvrDaXB82Y9DRFour0Nz/KrBh7reWw=
 go.opentelemetry.io/otel/internal/metric v0.27.0 h1:9dAVGAfFiiEq5NVB9FUJ5et+btbDQAUIJehJ+ikyryk=
 go.opentelemetry.io/otel/internal/metric v0.27.0 h1:9dAVGAfFiiEq5NVB9FUJ5et+btbDQAUIJehJ+ikyryk=
 go.opentelemetry.io/otel/internal/metric v0.27.0/go.mod h1:n1CVxRqKqYZtqyTh9U/onvKapPGv7y/rpyOTI+LFNzw=
 go.opentelemetry.io/otel/internal/metric v0.27.0/go.mod h1:n1CVxRqKqYZtqyTh9U/onvKapPGv7y/rpyOTI+LFNzw=
+go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
+go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
 go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw=
 go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw=
+go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
+go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
+go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
 go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g=
 go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g=
+go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
+go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI=
+go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
+go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
+go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
 go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk=
 go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk=
 go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU=
 go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU=
+go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
 go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
 go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
 go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
 go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
+golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo=
+golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
 golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
 golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
 golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
+golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
+golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
 golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
 google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
+google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
+google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
 google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA=
 google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA=
+google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
+google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
 google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
 google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
 google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY=
 google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY=
 google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk=
 google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk=
+google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
+google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
 google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
 google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
 google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
 google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
+google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM=
+google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
+google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
 google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
+google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
+google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
 gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
+gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
+gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
 gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
 gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
 gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs=
 gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
 gopkg.in/ghodss/yaml.v1 v1.0.0 h1:JlY4R6oVz+ZSvcDhVfNQ/k/8Xo6yb2s1PBhslPZPX4c=
 gopkg.in/ghodss/yaml.v1 v1.0.0 h1:JlY4R6oVz+ZSvcDhVfNQ/k/8Xo6yb2s1PBhslPZPX4c=
 gopkg.in/ghodss/yaml.v1 v1.0.0/go.mod h1:HDvRMPQLqycKPs9nWLuzZWxsxRzISLCRORiDpBUOMqg=
 gopkg.in/ghodss/yaml.v1 v1.0.0/go.mod h1:HDvRMPQLqycKPs9nWLuzZWxsxRzISLCRORiDpBUOMqg=
 gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
 gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
 gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
 gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
+gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
 gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
 gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A=
+gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
+gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
 gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
+gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
 gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
 gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
+gopkg.in/segmentio/analytics-go.v3 v3.1.0 h1:UzxH1uaGZRpMKDhJyBz0pexz6yUoBU3x8bJsRk/HV6U=
+gopkg.in/segmentio/analytics-go.v3 v3.1.0/go.mod h1:4QqqlTlSSpVlWA9/9nDcPw+FkM2yv1NQoYjUbL9/JAw=
+gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
 gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
+gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
+gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
+gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
+gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 helm.sh/helm/v3 v3.7.1 h1:kED/HWx09QHHSJhYaJY6ttj/BhmzBmT1oupKslncibY=
 helm.sh/helm/v3 v3.7.1 h1:kED/HWx09QHHSJhYaJY6ttj/BhmzBmT1oupKslncibY=
 helm.sh/helm/v3 v3.7.1/go.mod h1:3eOeBD3Z+O/ELiuu19zynZSN8jP1ErXLuyP21SZeMq8=
 helm.sh/helm/v3 v3.7.1/go.mod h1:3eOeBD3Z+O/ELiuu19zynZSN8jP1ErXLuyP21SZeMq8=
 honnef.co/go/tools v0.2.1 h1:/EPr//+UMMXwMTkXvCCoaJDq8cpjMO80Ou+L4PDo2mY=
 honnef.co/go/tools v0.2.1 h1:/EPr//+UMMXwMTkXvCCoaJDq8cpjMO80Ou+L4PDo2mY=
+istio.io/api v0.0.0-20221109202042-b9e5d446a83d h1:ufITkou8JPq7AtpCgJujuvQrBhIse+sRtD9+coG7BjI=
+istio.io/api v0.0.0-20221109202042-b9e5d446a83d/go.mod h1:hQkF0Q19MCmfOTre/Sg4KvrwwETq45oaFplnBm2p4j8=
+istio.io/client-go v1.16.0 h1:wIHRK9x1GbPm4AOeEMhHlpJL7uhNPhtVgzaxIGrIRGU=
+istio.io/client-go v1.16.0/go.mod h1:UV8SFeM2qNime5sobkr2m8oTCPxxVt9xCY4ol50U9YQ=
+k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
+k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
+k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
+k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
+k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
+k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
+k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
+k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
+k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
+k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
+k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0=
 k8s.io/code-generator v0.25.2 h1:qEHux0+E1c+j1MhsWn9+4Z6av8zrZBixOTPW064rSiY=
 k8s.io/code-generator v0.25.2 h1:qEHux0+E1c+j1MhsWn9+4Z6av8zrZBixOTPW064rSiY=
 k8s.io/code-generator v0.25.2/go.mod h1:f61OcU2VqVQcjt/6TrU0sta1TA5hHkOO6ZZPwkL9Eys=
 k8s.io/code-generator v0.25.2/go.mod h1:f61OcU2VqVQcjt/6TrU0sta1TA5hHkOO6ZZPwkL9Eys=
+k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
+k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
+k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
 k8s.io/component-helpers v0.25.2 h1:A4xQEFq7tbnhB3CTwZTLcQtyEhFFZN2TyQjNgziuSEI=
 k8s.io/component-helpers v0.25.2 h1:A4xQEFq7tbnhB3CTwZTLcQtyEhFFZN2TyQjNgziuSEI=
 k8s.io/component-helpers v0.25.2/go.mod h1:iuyfZG2jGWYvR5F/yGFUYNdL/IFz2smcwpNaOqP+YNM=
 k8s.io/component-helpers v0.25.2/go.mod h1:iuyfZG2jGWYvR5F/yGFUYNdL/IFz2smcwpNaOqP+YNM=
+k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
+k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=
 k8s.io/cri-api v0.23.1 h1:0DHL/hpTf4Fp+QkUXFefWcp1fhjXr9OlNdY9X99c+O8=
 k8s.io/cri-api v0.23.1 h1:0DHL/hpTf4Fp+QkUXFefWcp1fhjXr9OlNdY9X99c+O8=
 k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4=
 k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4=
+k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHzPMyB0t8BaFeBYI=
 k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHzPMyB0t8BaFeBYI=
 k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
+k8s.io/helm v2.17.0+incompatible h1:Bpn6o1wKLYqKM3+Osh8e+1/K2g/GsQJ4F4yNF2+deao=
+k8s.io/helm v2.17.0+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI=
+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
 k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8=
 k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8=
+k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
 k8s.io/metrics v0.25.2 h1:105TuPaIFfr4EHzN56WwZJO7r1UesuDytNTzeMqGySo=
 k8s.io/metrics v0.25.2 h1:105TuPaIFfr4EHzN56WwZJO7r1UesuDytNTzeMqGySo=
 k8s.io/metrics v0.25.2/go.mod h1:4NDAauOuEJ+NWO2+hWkhFE4rWBx/plLWJOYU3vGl0sA=
 k8s.io/metrics v0.25.2/go.mod h1:4NDAauOuEJ+NWO2+hWkhFE4rWBx/plLWJOYU3vGl0sA=
 k8s.io/sample-controller v0.22.1 h1:2C2d9VwoCurcHj3NsagyIEFc9HL3SlvPlHkvjF1F39Y=
 k8s.io/sample-controller v0.22.1 h1:2C2d9VwoCurcHj3NsagyIEFc9HL3SlvPlHkvjF1F39Y=
+k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
 modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
 modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
 modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
@@ -1085,11 +1436,17 @@ modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
 mvdan.cc/gofumpt v0.1.1 h1:bi/1aS/5W00E2ny5q65w9SnKpWEF/UIOqDYBILpo9rA=
 mvdan.cc/gofumpt v0.1.1 h1:bi/1aS/5W00E2ny5q65w9SnKpWEF/UIOqDYBILpo9rA=
 mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
 mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
 mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
 mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
+mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
+mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8=
 mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7 h1:HT3e4Krq+IE44tiN36RvVEb6tvqeIdtsVSsxmNPqlFU=
 mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7 h1:HT3e4Krq+IE44tiN36RvVEb6tvqeIdtsVSsxmNPqlFU=
 rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
 rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
 rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
 rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
 rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
 rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.32 h1:2WjukG7txtEsbXsSKWtTibCdsyYAhcu6KFnttyDdZOQ=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.32 h1:2WjukG7txtEsbXsSKWtTibCdsyYAhcu6KFnttyDdZOQ=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.32/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.32/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw=
 sigs.k8s.io/kustomize/kustomize/v4 v4.5.7 h1:cDW6AVMl6t/SLuQaezMET8hgnadZGIAr8tUrxFVOrpg=
 sigs.k8s.io/kustomize/kustomize/v4 v4.5.7 h1:cDW6AVMl6t/SLuQaezMET8hgnadZGIAr8tUrxFVOrpg=
 sigs.k8s.io/kustomize/kustomize/v4 v4.5.7/go.mod h1:VSNKEH9D9d9bLiWEGbS6Xbg/Ih0tgQalmPvntzRxZ/Q=
 sigs.k8s.io/kustomize/kustomize/v4 v4.5.7/go.mod h1:VSNKEH9D9d9bLiWEGbS6Xbg/Ih0tgQalmPvntzRxZ/Q=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=

+ 11 - 10
internal/deployment_target/get.go

@@ -11,10 +11,11 @@ import (
 
 
 // DeploymentTargetDetailsInput is the input to the DeploymentTargetDetails function
 // DeploymentTargetDetailsInput is the input to the DeploymentTargetDetails function
 type DeploymentTargetDetailsInput struct {
 type DeploymentTargetDetailsInput struct {
-	ProjectID          int64
-	ClusterID          int64
-	DeploymentTargetID string
-	CCPClient          porterv1connect.ClusterControlPlaneServiceClient
+	ProjectID            int64
+	ClusterID            int64
+	DeploymentTargetID   string
+	DeploymentTargetName string
+	CCPClient            porterv1connect.ClusterControlPlaneServiceClient
 }
 }
 
 
 // DeploymentTarget is a struct representing the unique cluster, namespace pair for a deployment target
 // DeploymentTarget is a struct representing the unique cluster, namespace pair for a deployment target
@@ -40,16 +41,16 @@ func DeploymentTargetDetails(ctx context.Context, inp DeploymentTargetDetailsInp
 	if inp.ProjectID == 0 {
 	if inp.ProjectID == 0 {
 		return deploymentTarget, telemetry.Error(ctx, span, nil, "project id is empty")
 		return deploymentTarget, telemetry.Error(ctx, span, nil, "project id is empty")
 	}
 	}
-	if inp.DeploymentTargetID == "" {
-		return deploymentTarget, telemetry.Error(ctx, span, nil, "deployment target id is empty")
-	}
 	if inp.CCPClient == nil {
 	if inp.CCPClient == nil {
 		return deploymentTarget, telemetry.Error(ctx, span, nil, "cluster control plane client is nil")
 		return deploymentTarget, telemetry.Error(ctx, span, nil, "cluster control plane client is nil")
 	}
 	}
 
 
 	deploymentTargetDetailsReq := connect.NewRequest(&porterv1.DeploymentTargetDetailsRequest{
 	deploymentTargetDetailsReq := connect.NewRequest(&porterv1.DeploymentTargetDetailsRequest{
-		ProjectId:          inp.ProjectID,
-		DeploymentTargetId: inp.DeploymentTargetID,
+		ProjectId: inp.ProjectID,
+		DeploymentTargetIdentifier: &porterv1.DeploymentTargetIdentifier{
+			Id:   inp.DeploymentTargetID,
+			Name: inp.DeploymentTargetName,
+		},
 	})
 	})
 
 
 	deploymentTargetDetailsResp, err := inp.CCPClient.DeploymentTargetDetails(ctx, deploymentTargetDetailsReq)
 	deploymentTargetDetailsResp, err := inp.CCPClient.DeploymentTargetDetails(ctx, deploymentTargetDetailsReq)
@@ -67,7 +68,7 @@ func DeploymentTargetDetails(ctx context.Context, inp DeploymentTargetDetailsInp
 	}
 	}
 
 
 	deploymentTarget = DeploymentTarget{
 	deploymentTarget = DeploymentTarget{
-		ID:        inp.DeploymentTargetID,
+		ID:        target.Id,
 		Name:      target.Name,
 		Name:      target.Name,
 		Namespace: target.Namespace,
 		Namespace: target.Namespace,
 		ClusterID: target.ClusterId,
 		ClusterID: target.ClusterId,

+ 7 - 2
zarf/helm/.dashboardenv

@@ -1,16 +1,21 @@
 # Fill out this file, and renamed to '.dashboard.env' in order to run this with Tilt
 # Fill out this file, and renamed to '.dashboard.env' in order to run this with Tilt
 
 
 # NODE_ENV denotes the environment that gets passed to webpack
 # NODE_ENV denotes the environment that gets passed to webpack
+
 NODE_ENV=development
 NODE_ENV=development
 
 
-# DEV_SERVER_PORT is the port that the UI will run on 
+# DEV_SERVER_PORT is the port that the UI will run on
+
 DEV_SERVER_PORT=8081
 DEV_SERVER_PORT=8081
 
 
 # ENABLE_PROXY decides if requests should be proxied
 # ENABLE_PROXY decides if requests should be proxied
+
 ENABLE_PROXY=true
 ENABLE_PROXY=true
 
 
 # API_SERVER is the URL that the API server is running on
 # API_SERVER is the URL that the API server is running on
+
 API_SERVER=http://localhost:8080
 API_SERVER=http://localhost:8080
 
 
 # TRUST_ARN is used with the cloudformation pack, to allow supporting multiple AWS accounts as management accounts. Change MY_AWS_DEV_ACCOUNT_ID to your AWS developer account ID
 # TRUST_ARN is used with the cloudformation pack, to allow supporting multiple AWS accounts as management accounts. Change MY_AWS_DEV_ACCOUNT_ID to your AWS developer account ID
-TRUST_ARN=arn:aws:iam::MY_AWS_DEV_ACCOUNT_ID:role/OrganizationAccountAccessRole
+
+TRUST_ARN=arn:aws:iam::MY_AWS_DEV_ACCOUNT_ID:role/CAPIManagement

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff