Sfoglia il codice sorgente

Merge branch 'master' of github.com:porter-dev/porter into stacks-v2-notifications

Feroze Mohideen 2 anni fa
parent
commit
efc639d849
45 ha cambiato i file con 813 aggiunte e 678 eliminazioni
  1. 44 5
      api/server/handlers/namespace/clone_env_group.go
  2. 1 1
      api/server/handlers/porter_app/analytics.go
  3. 0 1
      api/server/handlers/porter_app/validate.go
  4. 107 0
      api/server/handlers/project/images.go
  5. 27 0
      api/server/router/project.go
  6. 2 0
      api/types/project.go
  7. 133 3
      cli/cmd/docker/builder.go
  8. 18 19
      cli/cmd/v2/apply.go
  9. 21 7
      dashboard/package-lock.json
  10. 3 2
      dashboard/package.json
  11. 2 4
      dashboard/src/components/ProvisionerSettings.tsx
  12. 57 23
      dashboard/src/lib/hooks/useClusterResourceLimits.ts
  13. 24 2
      dashboard/src/lib/porter-apps/index.ts
  14. 1 1
      dashboard/src/lib/porter-apps/services.ts
  15. 1 0
      dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx
  16. 0 6
      dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx
  17. 40 3
      dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx
  18. 12 2
      dashboard/src/main/home/app-dashboard/apps/AppGrid.tsx
  19. 6 14
      dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx
  20. 18 9
      dashboard/src/main/home/app-dashboard/image-settings/ImageList.tsx
  21. 24 103
      dashboard/src/main/home/app-dashboard/image-settings/ImageSettings.tsx
  22. 30 61
      dashboard/src/main/home/app-dashboard/image-settings/TagList.tsx
  23. 8 7
      dashboard/src/main/home/app-dashboard/image-settings/types.ts
  24. 6 6
      dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts
  25. 41 9
      dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx
  26. 1 0
      dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts
  27. 3 0
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx
  28. 5 10
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx
  29. 65 4
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx
  30. 11 2
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx
  31. 11 1
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/WebTabs.tsx
  32. 7 0
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/utils.ts
  33. 0 10
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/AppTemplateForm.tsx
  34. 1 1
      dashboard/src/main/home/sidebar/ClusterList.tsx
  35. 3 1
      dashboard/src/shared/ClusterResourcesContext.tsx
  36. 10 0
      dashboard/src/shared/api.tsx
  37. 2 0
      dashboard/src/shared/types.tsx
  38. 3 2
      go.mod
  39. 9 5
      go.sum
  40. 4 346
      go.work.sum
  41. 1 0
      internal/integrations/ci/actions/stack.go
  42. 9 5
      internal/integrations/ci/actions/steps.go
  43. 14 1
      internal/models/project.go
  44. 19 2
      internal/porter_app/v2/yaml.go
  45. 9 0
      services/porter_cli_container/Dockerfile

+ 44 - 5
api/server/handlers/namespace/clone_env_group.go

@@ -15,6 +15,9 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 type CloneEnvGroupHandler struct {
@@ -34,6 +37,9 @@ func NewCloneEnvGroupHandler(
 }
 
 func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "clone-env-group-legacy")
+	defer span.End()
+
 	request := &types.CloneEnvGroupRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
@@ -45,13 +51,15 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	agent, err := c.GetAgent(r, cluster, "")
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error getting kubernetes agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
 	cm, _, err := agent.GetLatestVersionedConfigMap(request.SourceName, namespace)
 	if err != nil {
 		if errors.Is(err, kubernetes.IsNotFoundError) {
+			_ = telemetry.Error(ctx, span, err, "error finding latest config map")
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", request.SourceName, namespace), http.StatusNotFound,
 				"no config map found for envgroup",
@@ -59,13 +67,16 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 			return
 		}
 
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error getting latest config map")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
 	secret, _, err := agent.GetLatestVersionedSecret(request.SourceName, namespace)
 	if err != nil {
 		if errors.Is(err, kubernetes.IsNotFoundError) {
+			_ = telemetry.Error(ctx, span, err, "error finding latest secret")
+
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", request.SourceName, namespace), http.StatusNotFound,
 				"no k8s secret found for envgroup",
@@ -73,7 +84,8 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 			return
 		}
 
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error getting secret")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
@@ -94,6 +106,30 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		secretVars[key] = string(val)
 	}
 
+	_, err = agent.Clientset.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
+	if err != nil {
+		if !errors.Is(err, kubernetes.IsNotFoundError) {
+			err = telemetry.Error(ctx, span, err, "error getting namespace")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		_, err = agent.Clientset.CoreV1().Namespaces().Create(ctx, &v1.Namespace{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "Namespace",
+				APIVersion: "v1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name: namespace,
+			},
+		}, metav1.CreateOptions{})
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error creating namespace")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+	}
+
 	configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
 		Name:            request.TargetName,
 		Namespace:       request.TargetNamespace,
@@ -101,13 +137,16 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		SecretVariables: secretVars,
 	})
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		err = telemetry.Error(ctx, span, err, "error creating env group")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
 	envGroup, err := envgroup.ToEnvGroup(configMap)
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+
+		err = telemetry.Error(ctx, span, err, "error converting env group")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 

+ 1 - 1
api/server/handlers/porter_app/analytics.go

@@ -127,7 +127,7 @@ func TrackStackBuildStatus(
 	validateApplyV2 bool,
 	b64BuildLogs string,
 ) error {
-	_, span := telemetry.NewSpan(ctx, "track-build-status")
+	ctx, span := telemetry.NewSpan(ctx, "track-build-status")
 	defer span.End()
 
 	telemetry.WithAttributes(

+ 0 - 1
api/server/handlers/porter_app/validate.go

@@ -208,7 +208,6 @@ func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	}
 
 	b64 := base64.StdEncoding.EncodeToString(encoded)
-
 	response := &ValidatePorterAppResponse{
 		ValidatedBase64AppProto: b64,
 	}

+ 107 - 0
api/server/handlers/project/images.go

@@ -0,0 +1,107 @@
+package project
+
+import (
+	"net/http"
+	"time"
+
+	"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/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// ImagesHandler serves the /images endpoint
+type ImagesHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+// NewImagesHandler returns a new ImagesHandler
+func NewImagesHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ImagesHandler {
+	return &ImagesHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+// ImageArtifact is an instance of an image in an image repository
+type ImageArtifact struct {
+	Tag       string    `json:"tag"`
+	UpdatedAt time.Time `json:"updated_at"`
+}
+
+// Image is a representation of a docker image
+// To pull one of the image instances, you must use a string of format <image.uri>:<image.artifact.tag>
+type Image struct {
+	Uri       string          `json:"uri"`
+	Artifacts []ImageArtifact `json:"artifacts"`
+}
+
+// ImagesReponse is the response payload for the /images endpoint
+type ImagesReponse struct {
+	Images []Image `json:"images"`
+}
+
+// ServeHTTP handles the GET request to retrieve a list of images for a given project
+func (p *ImagesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-images")
+	defer span.End()
+
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	if project.ID == 0 {
+		err := telemetry.Error(ctx, span, nil, "project id is 0")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	var resp ImagesReponse
+
+	imagesReq := connect.NewRequest(&porterv1.ImagesRequest{
+		ProjectId: int64(project.ID),
+	})
+	ccpResp, err := p.Config().ClusterControlPlaneClient.Images(ctx, imagesReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error calling ccp rollback porter app")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if ccpResp == nil {
+		err := telemetry.Error(ctx, span, err, "ccp resp is nil")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if ccpResp.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "ccp resp msg is nil")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if ccpResp.Msg.Images == nil {
+		err := telemetry.Error(ctx, span, err, "ccp resp msg images is nil")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	for _, image := range ccpResp.Msg.Images {
+		var artifacts []ImageArtifact
+		for _, artifact := range image.Artifacts {
+			artifacts = append(artifacts, ImageArtifact{
+				Tag:       artifact.Tag,
+				UpdatedAt: artifact.UpdatedAt.AsTime().UTC(),
+			})
+		}
+		resp.Images = append(resp.Images, Image{
+			Uri:       image.Uri,
+			Artifacts: artifacts,
+		})
+	}
+
+	p.WriteResult(w, r, resp)
+}

+ 27 - 0
api/server/router/project.go

@@ -1429,5 +1429,32 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/images -> project.ImagesHandler
+	imagesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/images",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	imagesHandler := project.NewImagesHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: imagesEndpoint,
+		Handler:  imagesHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 2 - 0
api/types/project.go

@@ -35,6 +35,7 @@ type Project struct {
 	StacksEnabled          bool    `json:"stacks_enabled"`
 	CapiProvisionerEnabled bool    `json:"capi_provisioner_enabled"`
 	DBEnabled              bool    `json:"db_enabled"`
+	EFSEnabled             bool    `json:"efs_enabled"`
 	SimplifiedViewEnabled  bool    `json:"simplified_view_enabled"`
 	AzureEnabled           bool    `json:"azure_enabled"`
 	HelmValuesEnabled      bool    `json:"helm_values_enabled"`
@@ -42,6 +43,7 @@ type Project struct {
 	FullAddOns             bool    `json:"full_add_ons"`
 	EnableReprovision      bool    `json:"enable_reprovision"`
 	ValidateApplyV2        bool    `json:"validate_apply_v2"`
+	QuotaIncrease          bool    `json:"quota_increase"`
 }
 
 type FeatureFlags struct {

+ 133 - 3
cli/cmd/docker/builder.go

@@ -4,10 +4,13 @@ import (
 	"archive/tar"
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
+	"os/exec"
+	"path/filepath"
 	"time"
 
 	"github.com/docker/docker/api/types"
@@ -17,7 +20,7 @@ import (
 	"github.com/moby/moby/pkg/jsonmessage"
 	"github.com/moby/moby/pkg/stringid"
 	"github.com/moby/term"
-	"github.com/pkg/errors"
+	"mvdan.cc/sh/v3/shell"
 )
 
 type BuildOpts struct {
@@ -36,10 +39,14 @@ type BuildOpts struct {
 
 // BuildLocal
 func (a *Agent) BuildLocal(ctx context.Context, opts *BuildOpts) (err error) {
+	if os.Getenv("DOCKER_BUILDKIT") == "1" {
+		return buildLocalWithBuildkit(ctx, *opts)
+	}
+
 	dockerfilePath := opts.DockerfilePath
 
 	// attempt to read dockerignore file and paths
-	dockerIgnoreBytes, _ := ioutil.ReadFile(".dockerignore")
+	dockerIgnoreBytes, _ := os.ReadFile(".dockerignore")
 	var excludes []string
 
 	if len(dockerIgnoreBytes) != 0 {
@@ -67,7 +74,7 @@ func (a *Agent) BuildLocal(ctx context.Context, opts *BuildOpts) (err error) {
 	if !opts.IsDockerfileInCtx {
 		dockerfileCtx, err := os.Open(dockerfilePath)
 		if err != nil {
-			return errors.Errorf("unable to open Dockerfile: %v", err)
+			return fmt.Errorf("unable to open Dockerfile: %v", err)
 		}
 
 		defer dockerfileCtx.Close()
@@ -169,3 +176,126 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl
 	})
 	return buildCtx, randomName, nil
 }
+
+func buildLocalWithBuildkit(ctx context.Context, opts BuildOpts) error {
+	if _, err := exec.LookPath("docker"); err != nil {
+		return fmt.Errorf("unable to find docker binary in PATH for buildkit build: %w", err)
+	}
+
+	// prepare Dockerfile if the location isn't inside the build context
+	dockerfileName := opts.DockerfilePath
+	if !opts.IsDockerfileInCtx {
+		var err error
+		dockerfileName, err = injectDockerfileIntoBuildContext(opts.BuildContext, opts.DockerfilePath)
+		if err != nil {
+			return fmt.Errorf("unable to inject Dockerfile into build context: %w", err)
+		}
+	}
+	// parse any arguments
+	var extraDockerArgs []string
+	if buildkitArgs := os.Getenv("PORTER_BUILDKIT_ARGS"); buildkitArgs != "" {
+		parsedFields, err := shell.Fields(buildkitArgs, func(name string) string {
+			return os.Getenv(name)
+		})
+		if err != nil {
+			return fmt.Errorf("error while parsing buildkit args: %w", err)
+		}
+		extraDockerArgs = parsedFields
+	}
+
+	commandArgs := []string{
+		"build",
+		"-f", dockerfileName,
+		"--platform", "linux/amd64",
+		"--tag", fmt.Sprintf("%s:%s", opts.ImageRepo, opts.Tag),
+		"--cache-from", fmt.Sprintf("%s:%s", opts.ImageRepo, opts.CurrentTag),
+	}
+	for key, val := range opts.Env {
+		commandArgs = append(commandArgs, "--build-arg", fmt.Sprintf("%s=%s", key, val))
+	}
+
+	commandArgs = append(commandArgs, extraDockerArgs...)
+	// note: the path _must_ be the last argument
+	commandArgs = append(commandArgs, opts.BuildContext)
+
+	stdoutWriters := []io.Writer{os.Stdout}
+	stderrWriters := []io.Writer{os.Stderr}
+	if opts.LogFile != nil {
+		stdoutWriters = append(stdoutWriters, opts.LogFile)
+		stderrWriters = append(stderrWriters, opts.LogFile)
+	}
+
+	// #nosec G204 - The command is meant to be variable
+	cmd := exec.CommandContext(ctx, "docker", commandArgs...)
+	cmd.Dir = opts.BuildContext
+	cmd.Stdout = io.MultiWriter(stdoutWriters...)
+	cmd.Stderr = io.MultiWriter(stderrWriters...)
+	if err := cmd.Start(); err != nil {
+		return fmt.Errorf("unable to start the build command: %w", err)
+	}
+
+	exitCode := 0
+	execErr := cmd.Wait()
+	if execErr != nil {
+		if exitError, ok := execErr.(*exec.ExitError); ok {
+			exitCode = exitError.ExitCode()
+		}
+	}
+
+	if err := ctx.Err(); err != nil && err == context.Canceled {
+		return fmt.Errorf("build command canceled: %w", ctx.Err())
+	}
+
+	if err := ctx.Err(); err != nil {
+		return fmt.Errorf("error while running build: %w", err)
+	}
+
+	if exitCode != 0 {
+		return fmt.Errorf("build exited with non-zero exit code %d", exitCode)
+	}
+
+	if execErr != nil {
+		return fmt.Errorf("error while running build: %w", execErr)
+	}
+
+	return nil
+}
+
+func injectDockerfileIntoBuildContext(buildContext string, dockerfilePath string) (string, error) {
+	randomName := ".dockerfile." + stringid.GenerateRandomID()[:20]
+	data := map[string]func() ([]byte, error){
+		randomName: func() ([]byte, error) {
+			return os.ReadFile(filepath.Clean(dockerfilePath))
+		},
+		".dockerignore": func() ([]byte, error) {
+			dockerignorePath := filepath.Join(buildContext, ".dockerignore")
+			dockerignorePath = filepath.Clean(dockerignorePath)
+			if _, err := os.Stat(dockerignorePath); errors.Is(err, os.ErrNotExist) {
+				if err := os.WriteFile(dockerignorePath, []byte{}, os.FileMode(0o600)); err != nil {
+					return []byte{}, err
+				}
+			}
+
+			data, err := os.ReadFile(dockerignorePath)
+			if err != nil {
+				return data, err
+			}
+
+			b := bytes.NewBuffer(data)
+			b.WriteString(".dockerignore")
+			b.WriteString("\n" + randomName + "\n")
+			return b.Bytes(), nil
+		},
+	}
+
+	for filename, fn := range data {
+		bytes, err := fn()
+		if err != nil {
+			return randomName, fmt.Errorf("failed to get file contents: %w", err)
+		}
+
+		return randomName, os.WriteFile(filepath.Join(buildContext, filename), bytes, os.FileMode(0o600))
+	}
+
+	return randomName, nil
+}

+ 18 - 19
cli/cmd/v2/apply.go

@@ -132,26 +132,11 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 	// b64AppOverrides is the base64-encoded app proto with preview environment specific overrides and env groups
 	var b64AppOverrides string
 
-	if inp.PreviewApply {
-		var previewEnvVariables map[string]string
-		var previewEnvSecrets map[string]string
-
-		if overrides != nil {
-			b64AppOverrides = overrides.B64AppProto
-			previewEnvVariables = overrides.EnvVariables
-			previewEnvSecrets = overrides.EnvSecrets
-		}
-
-		envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, deploymentTargetID, previewEnvVariables, previewEnvSecrets, b64AppOverrides)
-		if err != nil {
-			return fmt.Errorf("error calling create or update app environment group endpoint: %w", err)
-		}
-		b64AppOverrides = envGroupResp.Base64AppProto
+	if inp.PreviewApply && overrides != nil {
+		b64AppOverrides = overrides.B64AppProto
 
-		b64AppOverrides, err = updateEnvGroupsInProto(ctx, b64AppOverrides, envGroupResp.EnvGroups)
-		if err != nil {
-			return fmt.Errorf("error updating app env group in proto: %w", err)
-		}
+		previewEnvVariables := overrides.EnvVariables
+		envVariables = mergeEnvVariables(envVariables, previewEnvVariables)
 	}
 
 	if appName == "" {
@@ -554,6 +539,20 @@ func imageTagFromBase64AppProto(base64AppProto string) (string, error) {
 	return app.Image.Tag, nil
 }
 
+func mergeEnvVariables(currentEnv, previousEnv map[string]string) map[string]string {
+	env := make(map[string]string)
+
+	for k, v := range previousEnv {
+		env[k] = v
+	}
+
+	for k, v := range currentEnv {
+		env[k] = v
+	}
+
+	return env
+}
+
 func updateEnvGroupsInProto(ctx context.Context, base64AppProto string, envGroups []environment_groups.EnvironmentGroup) (string, error) {
 	var editedB64AppProto string
 

+ 21 - 7
dashboard/package-lock.json

@@ -70,6 +70,7 @@
         "react-modal": "^3.11.2",
         "react-router-dom": "^5.2.0",
         "react-table": "^7.7.0",
+        "react-timer-hook": "^3.0.7",
         "react-transition-group": "^4.4.2",
         "regenerator-runtime": "^0.13.9",
         "semver": "^7.3.5",
@@ -89,7 +90,7 @@
         "@babel/preset-react": "^7.14.5",
         "@babel/preset-typescript": "^7.15.0",
         "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-        "@porter-dev/api-contracts": "^0.2.27",
+        "@porter-dev/api-contracts": "^0.2.28",
         "@testing-library/jest-dom": "^4.2.4",
         "@testing-library/react": "^9.3.2",
         "@testing-library/user-event": "^7.1.2",
@@ -2458,9 +2459,9 @@
       }
     },
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.2.27",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.27.tgz",
-      "integrity": "sha512-Zh8R4p+gPGYXeELP5rlUbkDLf0w7wgUPWeUHniKK7WyCQNIb4zrSImVl3PC1S12eUbRfT+Nry6Jj0VcktMN0Tw==",
+      "version": "0.2.28",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.28.tgz",
+      "integrity": "sha512-+UCD2ukvdjMkYEGCORSgXG3yZ6m//XzC9zEWuBf/p+iaztW5dM3vYaIDVpqDEzvndihLRekYrqVeD6cE/AoONQ==",
       "dev": true,
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
@@ -11399,6 +11400,14 @@
         "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
       }
     },
+    "node_modules/react-timer-hook": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/react-timer-hook/-/react-timer-hook-3.0.7.tgz",
+      "integrity": "sha512-ATpNcU+PQRxxfNBPVqce2+REtjGAlwmfoNQfcEBMZFxPj0r3GYdKhyPHdStvqrejejEi0QvqaJZjy2lBlFvAsA==",
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
     "node_modules/react-transition-group": {
       "version": "4.4.5",
       "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -16979,9 +16988,9 @@
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "@porter-dev/api-contracts": {
-      "version": "0.2.27",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.27.tgz",
-      "integrity": "sha512-Zh8R4p+gPGYXeELP5rlUbkDLf0w7wgUPWeUHniKK7WyCQNIb4zrSImVl3PC1S12eUbRfT+Nry6Jj0VcktMN0Tw==",
+      "version": "0.2.28",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.28.tgz",
+      "integrity": "sha512-+UCD2ukvdjMkYEGCORSgXG3yZ6m//XzC9zEWuBf/p+iaztW5dM3vYaIDVpqDEzvndihLRekYrqVeD6cE/AoONQ==",
       "dev": true,
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"
@@ -24223,6 +24232,11 @@
       "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
       "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA=="
     },
+    "react-timer-hook": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/react-timer-hook/-/react-timer-hook-3.0.7.tgz",
+      "integrity": "sha512-ATpNcU+PQRxxfNBPVqce2+REtjGAlwmfoNQfcEBMZFxPj0r3GYdKhyPHdStvqrejejEi0QvqaJZjy2lBlFvAsA=="
+    },
     "react-transition-group": {
       "version": "4.4.5",
       "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",

+ 3 - 2
dashboard/package.json

@@ -65,6 +65,7 @@
     "react-modal": "^3.11.2",
     "react-router-dom": "^5.2.0",
     "react-table": "^7.7.0",
+    "react-timer-hook": "^3.0.7",
     "react-transition-group": "^4.4.2",
     "regenerator-runtime": "^0.13.9",
     "semver": "^7.3.5",
@@ -94,7 +95,7 @@
     "@babel/preset-react": "^7.14.5",
     "@babel/preset-typescript": "^7.15.0",
     "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-    "@porter-dev/api-contracts": "^0.2.27",
+    "@porter-dev/api-contracts": "^0.2.28",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
@@ -150,4 +151,4 @@
     "webpack-cli": "^3.3.12",
     "webpack-dev-server": "^3.11.0"
   }
-}
+}

+ 2 - 4
dashboard/src/components/ProvisionerSettings.tsx

@@ -72,6 +72,7 @@ const regionOptions = [
   { value: "sa-east-1", label: "South America (São Paulo) sa-east-1" },
 ];
 
+// IMPORTANT: when adding more machineTypeOptions here, please make sure that you also enter their resources in useClusterResourceLimits.ts
 const machineTypeOptions = [
   { value: "t3.medium", label: "t3.medium" },
   { value: "t3.large", label: "t3.large" },
@@ -518,7 +519,6 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
     await requestQuotaIncrease()
     await createCluster()
   }
-
   const requestQuotaIncrease = async () => {
 
     try {
@@ -551,8 +551,6 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
 
   }
 
-
-
   const preflightChecks = async () => {
 
     try {
@@ -1112,7 +1110,7 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
                   <Spacer y={.5} />
                   {(preflightFailed && preflightData) &&
                     <>
-                      {(showHelpMessage) ? <>
+                      {(showHelpMessage && currentProject?.quota_increase) ? <>
                         <Text color="helper">
                           Your account currently is blocked from provisioning in {awsRegion} due to a quota limit imposed by AWS. Either change the region or request to increase quotas.
                         </Text>

+ 57 - 23
dashboard/src/lib/hooks/useClusterResourceLimits.ts

@@ -5,16 +5,20 @@ import { useQuery } from "@tanstack/react-query";
 import { z } from "zod";
 import api from "shared/api";
 
-const clusterDataValidator = z.object({
+const DEFAULT_INSTANCE_CLASS = "t3";
+const DEFAULT_INSTANCE_SIZE = "medium";
+
+const clusterNodesValidator = z.object({
     labels: z.object({
         "beta.kubernetes.io/instance-type": z.string().nullish(),
         "porter.run/workload-kind": z.string().nullish(),
     }).optional(),
 }).transform((data) => {
     const defaultResources = {
-        maxCPU: AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"],
-        maxRAM: AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"],
-        instanceType: "t3.medium",
+        maxCPU: AWS_INSTANCE_LIMITS[DEFAULT_INSTANCE_CLASS][DEFAULT_INSTANCE_SIZE]["vCPU"],
+        maxRAM: AWS_INSTANCE_LIMITS[DEFAULT_INSTANCE_CLASS][DEFAULT_INSTANCE_SIZE]["RAM"],
+        instanceClass: DEFAULT_INSTANCE_CLASS,
+        instanceSize: DEFAULT_INSTANCE_SIZE,
     };
     if (!data.labels) {
         return defaultResources;
@@ -34,7 +38,8 @@ const clusterDataValidator = z.object({
         return {
             maxCPU: vCPU,
             maxRAM: RAM,
-            instanceType: instanceType,
+            instanceClass,
+            instanceSize,
         };
     }
     return defaultResources;
@@ -55,33 +60,35 @@ export const useClusterResourceLimits = (
     defaultCPU: number,
     defaultRAM: number,
     clusterContainsGPUNodes: boolean,
+    clusterIngressIp: string,
 } => {
     const SMALL_INSTANCE_UPPER_BOUND = 0.75;
     const LARGE_INSTANCE_UPPER_BOUND = 0.9;
     const DEFAULT_MULTIPLIER = 0.125;
-    const [clusterContainsGPUNodes, setGpuNodes] = useState(false);
+    const [clusterContainsGPUNodes, setClusterContainsGPUNodes] = useState(false);
     const [maxCPU, setMaxCPU] = useState(
-        AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"] * SMALL_INSTANCE_UPPER_BOUND
-    ); //default is set to a t3 medium
+        AWS_INSTANCE_LIMITS[DEFAULT_INSTANCE_CLASS][DEFAULT_INSTANCE_SIZE]["vCPU"] * SMALL_INSTANCE_UPPER_BOUND
+    );
     const [maxRAM, setMaxRAM] = useState(
         // round to nearest 100
         Math.round(
-            convert(AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"], "GiB").to("MB") *
+            convert(AWS_INSTANCE_LIMITS[DEFAULT_INSTANCE_CLASS][DEFAULT_INSTANCE_SIZE]["RAM"], "GiB").to("MB") *
             SMALL_INSTANCE_UPPER_BOUND / 100
         ) * 100
-    ); //default is set to a t3 medium
+    );
     const [defaultCPU, setDefaultCPU] = useState(
-        AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"] * DEFAULT_MULTIPLIER
-    ); //default is set to a t3 medium
+        AWS_INSTANCE_LIMITS[DEFAULT_INSTANCE_CLASS][DEFAULT_INSTANCE_SIZE]["vCPU"] * DEFAULT_MULTIPLIER
+    );
     const [defaultRAM, setDefaultRAM] = useState(
         // round to nearest 100
         Math.round(
-            convert(AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"], "GiB").to("MB") *
+            convert(AWS_INSTANCE_LIMITS[DEFAULT_INSTANCE_CLASS][DEFAULT_INSTANCE_SIZE]["RAM"], "GiB").to("MB") *
             DEFAULT_MULTIPLIER / 100
         ) * 100
-    ); //default is set to a t3 medium
+    );
+    const [clusterIngressIp, setClusterIngressIp] = useState<string>("");
 
-    const { data } = useQuery(
+    const getClusterNodes = useQuery(
         ["getClusterNodes", projectId, clusterId],
         async () => {
             if (!projectId || !clusterId || clusterId === -1) {
@@ -97,7 +104,7 @@ export const useClusterResourceLimits = (
                 }
             )
 
-            return await z.array(clusterDataValidator).parseAsync(res.data);
+            return await z.array(clusterNodesValidator).parseAsync(res.data);
         },
         {
             enabled: !!projectId && !!clusterId,
@@ -105,9 +112,9 @@ export const useClusterResourceLimits = (
             retry: false,
         }
     );
-
     useEffect(() => {
-        if (data) {
+        if (getClusterNodes.isSuccess) {
+            const data = getClusterNodes.data;
             // this logic handles CPU and RAM independently - we might want to change this later
             const maxCPU = data.reduce((acc, curr) => {
                 return Math.max(acc, curr.maxCPU);
@@ -131,13 +138,39 @@ export const useClusterResourceLimits = (
             setMaxRAM(newMaxRAM);
             setDefaultCPU(Number((newMaxCPU * DEFAULT_MULTIPLIER).toFixed(2)));
             setDefaultRAM(Number((newMaxRAM * DEFAULT_MULTIPLIER).toFixed(0)));
+            setClusterContainsGPUNodes(data.some(item => item.instanceClass === "g4dn"));
+        }
+    }, [getClusterNodes])
 
-            // Check if any instance type has "gd4n" and update clusterContainsGPUNodes accordingly
-            setGpuNodes(data.some(item =>
-                item.instanceType.includes("g4dn")
-            ));
+    const getCluster = useQuery(
+        ["getCluster", projectId, clusterId],
+        async () => {
+            if (!projectId || !clusterId || clusterId === -1) {
+                return Promise.resolve({ ingress_ip: "" });
+            }
+
+            const res = await api.getCluster(
+                "<token>",
+                {},
+                {
+                    project_id: projectId,
+                    cluster_id: clusterId,
+                }
+            )
+
+            return await z.object({ ingress_ip: z.string() }).parseAsync(res.data);
+        },
+        {
+            enabled: !!projectId && !!clusterId,
+            refetchOnWindowFocus: false,
+            retry: false,
+        }
+    );
+    useEffect(() => {
+        if (getCluster.isSuccess) {
+            setClusterIngressIp(getCluster.data.ingress_ip);
         }
-    }, [data])
+    }, [getCluster])
 
 
     return {
@@ -146,6 +179,7 @@ export const useClusterResourceLimits = (
         defaultCPU,
         defaultRAM,
         clusterContainsGPUNodes,
+        clusterIngressIp,
     }
 }
 

+ 24 - 2
dashboard/src/lib/porter-apps/index.ts

@@ -15,6 +15,7 @@ import {
   HelmOverrides,
   PorterApp,
   Service,
+  EFS,
 } from "@porter-dev/api-contracts";
 import { match } from "ts-pattern";
 import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
@@ -68,11 +69,15 @@ export const clientAppValidator = z.object({
     value: z
       .string()
       .min(1, { message: "Name must be at least 1 character" })
-      .max(30, { message: "Name must be 30 characters or less" })
+      .max(31, { message: "Name must be 31 characters or less" })
       .regex(/^[a-z0-9-]{1,61}$/, {
         message: 'Lowercase letters, numbers, and "-" only.',
       }),
   }),
+  efsStorage: z.object({
+    enabled: z.boolean(),
+    readOnly: z.boolean().optional(),
+  }),
   envGroups: z
     .object({ name: z.string(), version: z.bigint() })
     .array()
@@ -280,7 +285,12 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
               app.helmOverrides != null
                 ? new HelmOverrides({ b64Values: btoa(app.helmOverrides) })
                 : undefined,
+
           }),
+          efsStorage:
+            new EFS({
+              enabled: app.efsStorage.enabled,
+            })
         })
     )
     .with(
@@ -301,6 +311,11 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
             app.helmOverrides != null
               ? new HelmOverrides({ b64Values: btoa(app.helmOverrides) })
               : undefined,
+          efsStorage:
+            new EFS({
+              enabled: app.efsStorage.enabled,
+            })
+
         })
     )
     .exhaustive();
@@ -439,6 +454,10 @@ export function clientAppFromProto({
         builder: "",
       },
       helmOverrides: helmOverrides,
+      efsStorage: new EFS({
+        enabled: proto.efsStorage?.enabled ?? false,
+      })
+
     };
   }
 
@@ -477,6 +496,10 @@ export function clientAppFromProto({
       builder: "",
     },
     helmOverrides: helmOverrides,
+    efsStorage:
+      { enabled: proto.efsStorage?.enabled ?? false }
+
+    ,
   };
 }
 
@@ -569,6 +592,5 @@ export function applyPreviewOverrides({
     }));
 
   app.env = [...env, ...additionalEnv];
-
   return app;
 }

+ 1 - 1
dashboard/src/lib/porter-apps/services.ts

@@ -262,7 +262,7 @@ export function serializeService(service: ClientService): SerializedService {
           }),
           healthCheck: serializeHealth({ health: config.healthCheck }),
           domains: config.domains.map((domain) => ({
-            name: domain.name.value,
+            name: domain.name.value.replace("https://", "").replace("http://", ""),
           })),
           ingressAnnotations: Object.fromEntries(
             config.ingressAnnotations

+ 1 - 0
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -333,6 +333,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
       }),
       source: resetSource,
       deletions: {
+        predeploy: [],
         envGroupNames: [],
         serviceNames: [],
       },

+ 0 - 6
dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx

@@ -59,9 +59,6 @@ const Overview: React.FC<Props> = ({ buttonStatus }) => {
             existingServiceNames={latestProto.predeploy ? ["pre-deploy"] : []}
             isPredeploy
             fieldArrayName={"app.predeploy"}
-            maxCPU={currentClusterResources.maxCPU}
-            maxRAM={currentClusterResources.maxRAM}
-            clusterContainsGPUNodes={currentClusterResources.clusterContainsGPUNodes}
           />
           <Spacer y={0.5} />
         </>
@@ -73,9 +70,6 @@ const Overview: React.FC<Props> = ({ buttonStatus }) => {
         fieldArrayName={"app.services"}
         existingServiceNames={Object.keys(latestProto.services)}
         serviceVersionStatus={serviceVersionStatus}
-        maxCPU={currentClusterResources.maxCPU}
-        maxRAM={currentClusterResources.maxRAM}
-        clusterContainsGPUNodes={currentClusterResources.clusterContainsGPUNodes}
         internalNetworkingDetails={{
           namespace: deploymentTarget.namespace,
           appName: porterApp.name,

+ 40 - 3
dashboard/src/main/home/app-dashboard/app-view/tabs/Settings.tsx

@@ -13,16 +13,23 @@ import { useAppAnalytics } from "lib/hooks/useAppAnalytics";
 import { useQueryClient } from "@tanstack/react-query";
 import { Context } from "shared/Context";
 import PreviewEnvironmentSettings from "./preview-environments/PreviewEnvironmentSettings";
+import { Controller, useFormContext } from "react-hook-form";
+import { PorterAppFormData } from "lib/porter-apps";
+import Checkbox from "components/porter/Checkbox";
 
 const Settings: React.FC = () => {
-  const { currentProject } = useContext(Context);
+  const { currentProject, currentCluster } = useContext(Context);
   const queryClient = useQueryClient();
   const history = useHistory();
   const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
   const { porterApp, clusterId, projectId, latestProto } = useLatestRevision();
   const { updateAppStep } = useAppAnalytics();
   const [isDeleting, setIsDeleting] = useState(false);
-
+  const {
+    control,
+    setValue,
+    watch
+  } = useFormContext<PorterAppFormData>();
   const [githubWorkflowFilename, setGithubWorkflowFilename] = useState(
     `porter_stack_${porterApp.name}.yml`
   );
@@ -142,8 +149,38 @@ const Settings: React.FC = () => {
       {currentProject?.preview_envs_enabled && !!latestProto.build ? (
         <PreviewEnvironmentSettings />
       ) : null}
+
+      {(currentCluster?.cloud_provider == "AWS" && currentProject?.efs_enabled) && <>
+        <Text size={16}>Enable shared storage across services for "{porterApp.name}"</Text>
+        <Spacer y={0.5} />
+        <Spacer y={.5} />
+        <Controller
+          name={`app.efsStorage`}
+          control={control}
+          render={({ field: { value, onChange } }) => (
+            <Checkbox
+              checked={value.enabled}
+              toggleChecked={() => {
+                onChange({
+                  ...value,
+                  enabled: !value.enabled,
+                },
+                );
+              }}
+              disabled={value.readOnly}
+              disabledTooltip={
+                "You may only edit this field in your porter.yaml."
+              }
+            >
+              <Text color="helper">
+                Enable EFS Storage
+              </Text>
+            </Checkbox>
+          )} />
+        <Spacer y={1} />
+      </>}
       <Text size={16}>Delete "{porterApp.name}"</Text>
-      <Spacer y={0.5} />
+      <Spacer y={.5} />
       <Text color="helper">
         Delete this application and all of its resources.
       </Text>

+ 12 - 2
dashboard/src/main/home/app-dashboard/apps/AppGrid.tsx

@@ -64,9 +64,19 @@ const AppGrid: React.FC<AppGridProps> = ({ apps, searchValue, view, sort }) => {
 
     return match(sort)
       .with("calendar", () =>
-        _.sortBy(filteredBySearch, ["last_deployed"]).reverse()
+        _.sortBy(filteredBySearch, [
+          (a) => {
+            return a.app_revision.updated_at;
+          },
+        ]).reverse()
+      )
+      .with("letter", () =>
+        _.sortBy(filteredBySearch, [
+          (a) => {
+            return a.source.name;
+          },
+        ])
       )
-      .with("letter", () => _.sortBy(filteredBySearch, ["name"]))
       .exhaustive();
   }, [appsWithProto, searchValue, sort]);
 

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

@@ -157,6 +157,9 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
           buildpacks: [],
         },
         env: [],
+        efsStorage: {
+          enabled: false,
+        }
       },
       source: {
         git_repo_name: "",
@@ -289,7 +292,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
             deployment_target_id: deploymentTarget.deployment_target_id,
             variables,
             secrets,
-            hard_env_update: true
+            hard_env_update: true,
           },
           {
             project_id: currentProject.id,
@@ -645,9 +648,8 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                             }
                           >
                             {detectedServices.count > 0
-                              ? `Detected ${detectedServices.count} service${
-                                  detectedServices.count > 1 ? "s" : ""
-                                } from porter.yaml.`
+                              ? `Detected ${detectedServices.count} service${detectedServices.count > 1 ? "s" : ""
+                              } from porter.yaml.`
                               : `Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.`}
                           </Text>
                         </AppearingDiv>
@@ -657,11 +659,6 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                     <ServiceList
                       addNewText={"Add a new service"}
                       fieldArrayName={"app.services"}
-                      maxCPU={currentClusterResources.maxCPU}
-                      maxRAM={currentClusterResources.maxRAM}
-                      clusterContainsGPUNodes={
-                        currentClusterResources.clusterContainsGPUNodes
-                      }
                     />
                   </>,
                   <>
@@ -695,11 +692,6 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                         })}
                         isPredeploy
                         fieldArrayName={"app.predeploy"}
-                        maxCPU={currentClusterResources.maxCPU}
-                        maxRAM={currentClusterResources.maxRAM}
-                        clusterContainsGPUNodes={
-                          currentClusterResources.clusterContainsGPUNodes
-                        }
                       />
                     </>
                   ),

+ 18 - 9
dashboard/src/main/home/app-dashboard/image-settings/ImageList.tsx

@@ -9,6 +9,7 @@ import SearchBar from "components/SearchBar";
 import Link from "components/porter/Link";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
+import _ from "lodash";
 
 type Props = {
   loading: boolean;
@@ -21,18 +22,15 @@ const ImageList: React.FC<Props> = ({
   loading,
   images,
 }) => {
-  const [error, setError] = useState<boolean>(false);
   const [searchFilter, setSearchFilter] = useState<string>("");
 
   const renderImageList = () => {
     if (loading) {
       return (
         <LoadingWrapper>
-          <Loading />
+          <Loading message={"Loading all images linked to your project"}/>
         </LoadingWrapper>
       );
-    } else if (error) {
-      return <LoadingWrapper>Error loading images</LoadingWrapper>;
     } else if (images.length === 0 && !searchFilter) {
       return <LoadingWrapper>
         <Text color="helper">No linked images found.</Text>
@@ -54,9 +52,21 @@ const ImageList: React.FC<Props> = ({
           return aIndex - bIndex;
         })
       : images.sort((a, b) => {
+        const mostRecentTagA = _.maxBy(a.artifacts, (artifact) => {
+          return new Date(artifact.updated_at ?? "").getTime();
+        });
+        const mostRecentTagB = _.maxBy(b.artifacts, (artifact) => {
+          return new Date(artifact.updated_at ?? "").getTime();
+        });
+        if (!mostRecentTagA) {
+          return 1;
+        }
+        if (!mostRecentTagB) {
+          return -1;
+        }
         return (
-          new Date(b.created_at ?? "").getTime() -
-          new Date(a.created_at ?? "").getTime()
+          new Date(mostRecentTagB.updated_at ?? "").getTime() -
+          new Date(mostRecentTagA.updated_at ?? "").getTime()
         );
       });
 
@@ -80,8 +90,7 @@ const ImageList: React.FC<Props> = ({
           onClick={() => {
             setSelectedImage({
               uri: searchFilter,
-              name: searchFilter,
-              registry_id: 0,
+              artifacts: [],
             });
           }}
         >
@@ -97,7 +106,7 @@ const ImageList: React.FC<Props> = ({
     <>
       <SearchBar
         setSearchFilter={setSearchFilter}
-        disabled={error || loading}
+        disabled={loading}
         prompt={"Search images..."}
       />
       <ExpandedWrapper>

+ 24 - 103
dashboard/src/main/home/app-dashboard/image-settings/ImageSettings.tsx

@@ -8,7 +8,7 @@ import Input from "components/porter/Input";
 import { z } from "zod";
 import ImageList from "./ImageList";
 import TagList from "./TagList";
-import { ImageType } from "./types";
+import { ImageType, imageValidator } from "./types";
 
 type Props = {
     projectId: number;
@@ -29,49 +29,26 @@ const ImageSettings: React.FC<Props> = ({
 }) => {
     const [images, setImages] = useState<ImageType[]>([]);
     const [selectedImage, setSelectedImage] = useState<ImageType | undefined>(undefined);
-    const { data: registries, isLoading: isLoadingRegistries } = useQuery(
-        ["getProjectRegistries", projectId],
+    const resp = useQuery(
+        ["getImages", projectId],
         async () => {
-            const res = await api.getProjectRegistries("<token>", {}, { id: projectId });
-            return await z.array(z.object({ id: z.number() })).parseAsync(res.data);
+            const res = await api.images("<token>", {}, { project_id: projectId });
+            return await z.object({ images: z.array(imageValidator) }).parseAsync(res.data);
         },
         {
             refetchOnWindowFocus: false,
         }
-    )
-
-    const { data: imageResp, isLoading: isLoadingImages } = useQuery(
-        ["getImages", projectId, imageTag, imageUri],
-        async () => {
-            if (registries == null) {
-                return [];
-            }
-            return (await Promise.all(registries.map(async ({ id: registry_id }: { id: number }) => {
-                const res = await api.getImageRepos("<token>", {}, {
-                    project_id: projectId,
-                    registry_id,
-                });
-                const parsed = await z.array(z.object({
-                    uri: z.string(),
-                    name: z.string(),
-                })).parseAsync(res.data);
-                return parsed.map(p => ({ ...p, registry_id }))
-            }))).flat();
-        },
-        {
-            enabled: !!registries,
-            refetchOnWindowFocus: false,
-        }
     );
-
+    
     useEffect(() => {
-        if (imageResp) {
-            setImages(imageResp);
+        if (resp.isSuccess) {
+            const images = resp.data.images;
+            setImages(images);
             if (imageUri) {
-                setSelectedImage(imageResp.find((image) => image.uri === imageUri));
+                setSelectedImage(images.find((image) => image.uri === imageUri));
             }
         }
-    }, [imageResp]);
+    }, [resp]);
 
     return (
         <div>
@@ -88,14 +65,13 @@ const ImageSettings: React.FC<Props> = ({
                                 setImageUri(image.uri);
                             }}
                             images={images}
-                            loading={isLoadingImages || isLoadingRegistries}
+                            loading={resp.isLoading}
                         />
                     </ExpandedWrapper>
                     <DarkMatter antiHeight="-4px" />
                     <Spacer y={0.3} />
                 </>
             )}
-
             {imageUri && (
                 <>
                     <Input
@@ -103,7 +79,6 @@ const ImageSettings: React.FC<Props> = ({
                         label="Image URL:"
                         width="100%"
                         value={selectedImage?.uri ?? imageUri}
-                        setValue={() => { }}
                         placeholder=""
                     />
                     <BackButton
@@ -114,24 +89,7 @@ const ImageSettings: React.FC<Props> = ({
                         Select image URL
                     </BackButton>
                     <Spacer y={1} />
-                    {!imageTag && (
-                        <>
-                            <Text color="helper">Specify your image tag.</Text>
-                            <Spacer y={0.5} />
-                            <ExpandedWrapper>
-                                <TagList
-                                    selectedImage={selectedImage}
-                                    projectId={projectId}
-                                    setSelectedTag={
-                                        (tag: string) => {
-                                            setImageTag(tag);
-                                        }
-                                    }
-                                />
-                            </ExpandedWrapper>
-                        </>
-                    )}
-                    {imageTag && (
+                    {imageTag ? (
                         <>
                             <Input
                                 disabled={true}
@@ -152,6 +110,17 @@ const ImageSettings: React.FC<Props> = ({
                                 Select image tag
                             </BackButton>
                         </>
+                    ) : (
+                        <>
+                            <Text color="helper">Specify your image tag.</Text>
+                            <Spacer y={0.5} />
+                            <ExpandedWrapper>
+                                <TagList
+                                    selectedImage={selectedImage}
+                                    setSelectedTag={setImageTag}
+                                />
+                            </ExpandedWrapper>
+                        </>
                     )}
                 </>
             )}
@@ -198,51 +167,3 @@ const BackButton = styled.div`
     margin-right: 6px;
   }
 `;
-
-const StyledAdvancedBuildSettings = styled.div`
-  color: ${({ showSettings }) => (showSettings ? "white" : "#aaaabb")};
-  background: ${({ theme }) => theme.fg};
-  border: 1px solid #494b4f;
-  :hover {
-    border: 1px solid #7a7b80;
-    color: white;
-  }
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  border-radius: 5px;
-  height: 40px;
-  font-size: 13px;
-  width: 100%;
-  padding-left: 10px;
-  cursor: pointer;
-  border-bottom-left-radius: ${({ showSettings }) => showSettings && "0px"};
-  border-bottom-right-radius: ${({ showSettings }) => showSettings && "0px"};
-  .dropdown {
-    margin-right: 8px;
-    font-size: 20px;
-    cursor: pointer;
-    border-radius: 20px;
-    transform: ${(props: { showSettings: boolean; isCurrent: boolean }) =>
-        props.showSettings ? "" : "rotate(-90deg)"};
-  }
-`;
-
-const AdvancedBuildTitle = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const StyledSourceBox = styled.div`
-  width: 100%;
-  color: #ffffff;
-  padding: 25px 35px 25px;
-  position: relative;
-  font-size: 13px;
-  border-radius: 5px;
-  background: ${(props) => props.theme.fg};
-  border: 1px solid #494b4f;
-  border-top: 0px;
-  border-top-left-radius: 0px;
-  border-top-right-radius: 0px;
-`;

+ 30 - 61
dashboard/src/main/home/app-dashboard/image-settings/TagList.tsx

@@ -1,102 +1,72 @@
-import React, { useEffect, useState } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
 import tag_icon from "assets/tag.png";
 import addCircle from "assets/add-circle.png";
-
-import api from "shared/api";
-import Loading from "components/Loading";
-import { ImageType, TagType, tagValidator } from "./types";
-import { useQuery } from "@tanstack/react-query";
+import { ArtifactType, ImageType } from "./types";
 import SearchBar from "components/SearchBar";
-import { z } from "zod";
 
 type Props = {
   selectedImage?: ImageType;
-  projectId: number;
   setSelectedTag: (x: string) => void;
 };
 
 const TagList: React.FC<Props> = ({
   selectedImage,
-  projectId,
   setSelectedTag,
 }) => {
-  const [tags, setTags] = useState<TagType[]>([]);
   const [searchFilter, setSearchFilter] = useState<string>("");
 
-  const { data: tagResp, isLoading, error } = useQuery(
-    ["getImageTags", selectedImage],
-    async () => {
-      if (!selectedImage) {
-        return;
+  const renderTagList = () => {
+    if (selectedImage == null) {
+      if (searchFilter) {
+        return (
+          <TagItem
+            onClick={() => {
+              setSelectedTag(searchFilter);
+            }}
+          >
+            <img src={addCircle} />
+            {`Use tag \"${searchFilter}\"`}
+          </TagItem>
+        );
       }
-
-      const res = await api.getImageTags(
-        "<token>",
-        {},
-        {
-          project_id: projectId,
-          registry_id: selectedImage.registry_id,
-          repo_name: selectedImage.name,
-        }
-      );
-      return z.array(tagValidator).parseAsync(res.data);
-    },
-    {
-      enabled: !!selectedImage && selectedImage.registry_id !== 0,
-      refetchOnWindowFocus: false,
+      return <LoadingWrapper>Please specify an tag.</LoadingWrapper>;
     }
-  )
-
-  useEffect(() => {
-    if (tagResp) {
-      setTags(tagResp);
-    }
-  }, [tagResp])
-
-  const renderTagList = () => {
-    if (isLoading && selectedImage && selectedImage.registry_id !== 0) {
-      return (
-        <LoadingWrapper>
-          <Loading />
-        </LoadingWrapper>
-      );
-    } else if (error) {
-      return <LoadingWrapper>Error loading tags.</LoadingWrapper>;
-    } else if (tags.length === 0 && !searchFilter) {
-      return <LoadingWrapper>Please specify a tag.</LoadingWrapper>;
+    
+    if (selectedImage.artifacts.length === 0 && !searchFilter) {
+      return <LoadingWrapper>Image has no tags; please specify a different image.</LoadingWrapper>;
     }
 
-    const sortedTags = searchFilter
-      ? tags
-        .filter((tag) => tag.tag.toLowerCase().includes(searchFilter.toLowerCase()))
+    const sortedArtifacts = searchFilter
+      ? selectedImage.artifacts
+        .filter(({ tag }) => tag.toLowerCase().includes(searchFilter.toLowerCase()))
         .sort((a, b) => {
           const aIndex = a.tag.toLowerCase().indexOf(searchFilter.toLowerCase());
           const bIndex = b.tag.toLowerCase().indexOf(searchFilter.toLowerCase());
           return aIndex - bIndex;
         })
-      : tags.sort((a, b) => {
+      : selectedImage.artifacts.sort((a, b) => {
         return (
-          new Date(b.pushed_at ?? "").getTime() -
-          new Date(a.pushed_at ?? "").getTime()
+          new Date(b.updated_at ?? "").getTime() -
+          new Date(a.updated_at ?? "").getTime()
         );
       })
 
-    const tagCards = sortedTags.map((tag: TagType, i: number) => {
+    const tagCards = sortedArtifacts.map((artifact: ArtifactType, i: number) => {
       return (
         <TagItem
           key={i}
           onClick={() => {
-            setSelectedTag(tag.tag);
+            setSelectedTag(artifact.tag);
           }}
         >
           <img src={tag_icon} />
-          {tag.tag}
+          {artifact.tag}
         </TagItem>
       );
     });
 
-    if (searchFilter !== "" && !tags.some((tag) => tag.tag === searchFilter)) {
+    if (searchFilter !== "" && !sortedArtifacts.some(({ tag }) => tag === searchFilter)) {
       tagCards.push(
         <TagItem
           onClick={() => {
@@ -116,7 +86,7 @@ const TagList: React.FC<Props> = ({
     <>
       <SearchBar
         setSearchFilter={setSearchFilter}
-        disabled={error != null || isLoading}
+        disabled={false}
         prompt={"Search tags..."}
       />
       <ExpandedWrapper>
@@ -170,5 +140,4 @@ const LoadingWrapper = styled.div`
   align-items: center;
   justify-content: center;
   font-size: 13px;
-  color: #ffffff44;
 `;

+ 8 - 7
dashboard/src/main/home/app-dashboard/image-settings/types.ts

@@ -1,12 +1,13 @@
 import { z } from "zod";
 
+const artifactValidator = z.object({
+    tag: z.string(),
+    updated_at: z.string(),
+})
+export type ArtifactType = z.infer<typeof artifactValidator>;
+
 export const imageValidator = z.object({
     uri: z.string(),
-    name: z.string(),
-    created_at: z.string().optional(),
-    registry_id: z.number(),
+    artifacts: z.array(artifactValidator),
 })
-export type ImageType = z.infer<typeof imageValidator>;
-
-export const tagValidator = z.object({ tag: z.string(), pushed_at: z.string() })
-export type TagType = z.infer<typeof tagValidator>;
+export type ImageType = z.infer<typeof imageValidator>;

+ 6 - 6
dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts

@@ -39,11 +39,11 @@ jobs:
     - name: Set Github tag
       id: vars
       run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+    - name: Setup porter
+      uses: porter-dev/setup-porter@v0.1.0
     - name: Deploy stack
       timeout-minutes: 30
-      uses: porter-dev/porter-cli-action@v0.1.0
-      with:
-        command: apply -f ${porterYamlPath}
+      run: porter apply -f ${porterYamlPath}
       env:
         PORTER_CLUSTER: ${clusterId}
         PORTER_HOST: https://dashboard.getporter.dev
@@ -81,11 +81,11 @@ jobs:
     - name: Set Github tag
       id: vars
       run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+    - name: Setup porter
+      uses: porter-dev/setup-porter@v0.1.0
     - name: Build and deploy preview environment
       timeout-minutes: 30
-      uses: porter-dev/porter-cli-action@v0.1.0
-      with:
-        command: apply -f ${porterYamlPath} --preview
+      run: porter apply -f ${porterYamlPath} --preview
       env:
         PORTER_CLUSTER: ${clusterId}
         PORTER_HOST: https://dashboard.getporter.dev

+ 41 - 9
dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx

@@ -4,9 +4,7 @@ import React, {
     useRef,
     useState,
 } from "react";
-
 import styled from "styled-components";
-
 import spinner from "assets/loading.gif";
 import api from "shared/api";
 import { useLogs } from "./utils";
@@ -27,6 +25,9 @@ import { useRevisionList } from "lib/hooks/useRevisionList";
 import { useLocation } from "react-router";
 import { useLatestRevision } from "../../app-view/LatestRevisionContext";
 import Filter from "components/porter/Filter";
+import { useTimer } from 'react-timer-hook';
+import { useIntercom } from "lib/hooks/useIntercom";
+import axios from "axios";
 
 type Props = {
     projectId: number;
@@ -44,6 +45,8 @@ type Props = {
     appId: number;
 };
 
+const DEFAULT_LOG_TIMEOUT_SECONDS = 60;
+
 const Logs: React.FC<Props> = ({
     projectId,
     clusterId,
@@ -65,7 +68,7 @@ const Logs: React.FC<Props> = ({
         revision_id: queryParams.get('revision_id'),
     }
 
-    const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
+    const scrollToBottomRef = useRef<HTMLDivElement | null>(null);
     const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
     const [enteredSearchText, setEnteredSearchText] = useState("");
     const [searchText, setSearchText] = useState("");
@@ -88,6 +91,8 @@ const Logs: React.FC<Props> = ({
     const { revisionIdToNumber } = useRevisionList({ appName, deploymentTargetId, projectId, clusterId });
     const { latestRevision: { revision_number: latestRevisionNumber } } = useLatestRevision();
 
+    const { showIntercomWithMessage } = useIntercom();
+
     const isAgentVersionUpdated = (agentImage: string | undefined) => {
         if (agentImage == null) {
             return false;
@@ -182,7 +187,7 @@ const Logs: React.FC<Props> = ({
         }, 5000);
     };
 
-    const { logs, refresh, moveCursor, paginationInfo } = useLogs({
+    const { logs, refresh, moveCursor, paginationInfo, stopLogStream } = useLogs({
         projectID: projectId,
         clusterID: clusterId,
         selectedFilterValues,
@@ -199,6 +204,20 @@ const Logs: React.FC<Props> = ({
         appID: appId,
     });
 
+    const { totalSeconds, isRunning, pause: pauseLogTimeout, restart: restartLogTimeout } = useTimer({
+        expiryTimestamp: dayjs().add(DEFAULT_LOG_TIMEOUT_SECONDS, 'seconds').toDate(),
+        onExpire: () => {
+            stopLogStream();
+            showIntercomWithMessage({ message: "I am having trouble receiving logs from my application." });
+        }
+    });
+
+    useEffect(() => {
+        if (logs.length) {
+            pauseLogTimeout();
+        }
+    },[logs.length]);
+
     useEffect(() => {
         setFilters([
             {
@@ -309,6 +328,7 @@ const Logs: React.FC<Props> = ({
                         <Spacer inline x={1} />
                         <ScrollButton
                             onClick={() => {
+                                restartLogTimeout(dayjs().add(DEFAULT_LOG_TIMEOUT_SECONDS, 'seconds').toDate());
                                 refresh({ isLive: selectedDate == null && timeRange?.endTime == null });
                             }}
                         >
@@ -320,7 +340,7 @@ const Logs: React.FC<Props> = ({
                 <Spacer y={0.5} />
                 <LogsSectionWrapper>
                     <StyledLogsSection>
-                        {isLoading && <Loading message="Waiting for logs..." />}
+                        {isLoading && <Loading message={"Initializing..."} />}
                         {!isLoading && logs.length !== 0 && (
                             <>
                                 <LoadMoreButton
@@ -355,8 +375,20 @@ const Logs: React.FC<Props> = ({
                                 </Highlight>
                             </Message>
                         )}
-                        {!isLoading && logs.length === 0 && selectedDate == null && (
-                            <Loading message="Waiting for logs..." />
+                        {!isLoading && logs.length === 0 && selectedDate == null && isRunning && (
+                            <Loading message={`Waiting ${totalSeconds} seconds for logs...`} />
+                        )}
+                        {!isLoading && logs.length === 0 && selectedDate == null && !isRunning && (
+                            <Message>
+                                Timed out waiting for logs.
+                                <Highlight onClick={() => {
+                                    restartLogTimeout(dayjs().add(DEFAULT_LOG_TIMEOUT_SECONDS, 'seconds').toDate());
+                                    refresh({ isLive: selectedDate == null && timeRange?.endTime == null });
+                                }}>
+                                    <i className="material-icons">autorenew</i>
+                                    Refresh
+                                </Highlight>
+                            </Message>
                         )}
                         <div ref={scrollToBottomRef} />
                     </StyledLogsSection>
@@ -400,7 +432,7 @@ const Logs: React.FC<Props> = ({
                 notify("Porter agent is outdated. Please upgrade to see logs.");
             }
         } catch (err) {
-            if (err.response?.status === 404) {
+            if (axios.isAxiosError(err) && err.response?.status === 404) {
                 setHasPorterAgent(false);
             }
         }
@@ -534,8 +566,8 @@ const Message = styled.div`
   justify-content: center;
   margin-left: 75px;
   text-align: center;
-  color: #ffffff44;
   font-size: 13px;
+  color: #aaaabb;
 `;
 
 const Highlight = styled.div`

+ 1 - 0
dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts

@@ -501,5 +501,6 @@ export const useLogs = ({
     refresh,
     moveCursor,
     paginationInfo,
+    stopLogStream: closeAllWebsockets,
   };
 };

+ 3 - 0
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx

@@ -32,6 +32,7 @@ interface ServiceProps {
     namespace: string;
     appName: string;
   };
+  clusterIngressIp: string;
 }
 
 const ServiceContainer: React.FC<ServiceProps> = ({
@@ -44,6 +45,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   maxRAM,
   clusterContainsGPUNodes,
   internalNetworkingDetails,
+  clusterIngressIp, 
 }) => {
   const [height, setHeight] = useState<Height>(service.expanded ? "auto" : 0);
 
@@ -77,6 +79,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
           maxRAM={maxRAM}
           clusterContainsGPUNodes={clusterContainsGPUNodes}
           internalNetworkingDetails={internalNetworkingDetails}
+          clusterIngressIp={clusterIngressIp}
         />
       ))
       .with({ config: { type: "worker" } }, (svc) => (

+ 5 - 10
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx

@@ -49,9 +49,6 @@ type ServiceListProps = {
   existingServiceNames?: string[];
   fieldArrayName: "app.services" | "app.predeploy";
   serviceVersionStatus?: Record<string, PorterAppVersionStatus[]>;
-  maxCPU: number;
-  maxRAM: number;
-  clusterContainsGPUNodes: boolean;
   internalNetworkingDetails?: {
     namespace: string;
     appName: string;
@@ -61,13 +58,10 @@ type ServiceListProps = {
 const ServiceList: React.FC<ServiceListProps> = ({
   addNewText,
   prePopulateService,
+  fieldArrayName,
   isPredeploy = false,
   existingServiceNames = [],
-  fieldArrayName,
   serviceVersionStatus,
-  maxCPU,
-  maxRAM,
-  clusterContainsGPUNodes,
   internalNetworkingDetails = {
     namespace: "",
     appName: "",
@@ -76,7 +70,7 @@ const ServiceList: React.FC<ServiceListProps> = ({
   // top level app form
   const { control: appControl } = useFormContext<PorterAppFormData>();
 
-  const { currentClusterResources } = useClusterResources();
+  const { currentClusterResources: {maxCPU, maxRAM, clusterContainsGPUNodes, clusterIngressIp, defaultCPU, defaultRAM} } = useClusterResources();
 
   // add service modal form
   const {
@@ -186,8 +180,8 @@ const ServiceList: React.FC<ServiceListProps> = ({
       deserializeService({
         service: defaultSerialized({
           ...data,
-          defaultCPU: currentClusterResources.defaultCPU,
-          defaultRAM: currentClusterResources.defaultRAM,
+          defaultCPU,
+          defaultRAM,
         }),
         expanded: true,
       })
@@ -223,6 +217,7 @@ const ServiceList: React.FC<ServiceListProps> = ({
                 maxRAM={maxRAM}
                 clusterContainsGPUNodes={clusterContainsGPUNodes}
                 internalNetworkingDetails={internalNetworkingDetails}
+                clusterIngressIp={clusterIngressIp}
               />
             ) : null;
           })}

+ 65 - 4
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx

@@ -2,15 +2,26 @@ import React from "react";
 import Button from "components/porter/Button";
 import styled from "styled-components";
 import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
 import { useFieldArray, useFormContext } from "react-hook-form";
 import { PorterAppFormData } from "lib/porter-apps";
 import { ControlledInput } from "components/porter/ControlledInput";
+import CopyToClipboard from "components/CopyToClipboard";
+import copy from "assets/copy-left.svg";
 
 interface Props {
   index: number;
+  clusterIngressIp: string;
 }
 
-const CustomDomains: React.FC<Props> = ({ index }) => {
+const isCustomDomain = (domain: string) => {
+  return !domain.includes("onporter.run") && !domain.includes("withporter.run");
+}
+
+const CustomDomains: React.FC<Props> = ({ 
+  index, 
+  clusterIngressIp,
+ }) => {
   const { control, register } = useFormContext<PorterAppFormData>();
   const { remove, append, fields } = useFieldArray({
     control,
@@ -33,8 +44,7 @@ const CustomDomains: React.FC<Props> = ({ index }) => {
       {fields.length !== 0 && (
         <>
           {fields.map((customDomain, i) => {
-            return !customDomain.name.value.includes("onporter.run") &&
-              !customDomain.name.value.includes("withporter.run") ? (
+            return isCustomDomain(customDomain.name.value) && (
               <div key={customDomain.id}>
                 <AnnotationContainer>
                   <ControlledInput
@@ -61,7 +71,7 @@ const CustomDomains: React.FC<Props> = ({ index }) => {
                 </AnnotationContainer>
                 <Spacer y={0.25} />
               </div>
-            ) : null;
+            );
           })}
         </>
       )}
@@ -77,6 +87,24 @@ const CustomDomains: React.FC<Props> = ({ index }) => {
       >
         + Add Custom Domain
       </Button>
+      {clusterIngressIp !== "" && (
+        <>
+          <Spacer y={0.5} />
+          <div style={{width: "550px"}}>
+            <Text color="helper">To configure a custom domain, you must add a CNAME record pointing to the following Ingress IP for your cluster: </Text>
+          </div>
+          <Spacer y={0.5} />
+          <IdContainer>
+            <Code>{clusterIngressIp}</Code>
+            <CopyContainer>
+                <CopyToClipboard text={clusterIngressIp}>
+                    <CopyIcon src={copy} alt="copy" />
+                </CopyToClipboard>
+            </CopyContainer>
+          </IdContainer>
+          <Spacer y={0.5} />
+        </>
+      )}
     </CustomDomainsContainer>
   );
 };
@@ -112,3 +140,36 @@ const DeleteButton = styled.div`
     }
   }
 `;
+
+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;
+`;
+
+const CopyIcon = styled.img`
+  cursor: pointer;
+  margin-left: 5px;
+  margin-right: 5px;
+  width: 15px;
+  height: 15px;
+  :hover {
+    opacity: 0.8;
+  }
+`;

+ 11 - 2
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx

@@ -23,9 +23,15 @@ type NetworkingProps = {
     namespace: string;
     appName: string;
   };
+  clusterIngressIp: string;
 };
 
-const Networking: React.FC<NetworkingProps> = ({ index, service, internalNetworkingDetails: {namespace, appName} }) => {
+const Networking: React.FC<NetworkingProps> = ({ 
+  index, 
+  service, 
+  internalNetworkingDetails: { namespace, appName }, 
+  clusterIngressIp,
+}) => {
   const { register, control, watch } = useFormContext<PorterAppFormData>();
 
   const privateService = watch(`app.services.${index}.config.private.value`);
@@ -132,7 +138,10 @@ const Networking: React.FC<NetworkingProps> = ({ index, service, internalNetwork
             </a>
           </Text>
           <Spacer y={0.5} />
-          <CustomDomains index={index} />
+          <CustomDomains 
+            index={index} 
+            clusterIngressIp={clusterIngressIp} 
+          />
           <Spacer y={0.5} />
           <Text color="helper">
             Ingress Custom Annotations

+ 11 - 1
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/WebTabs.tsx

@@ -23,9 +23,18 @@ interface Props {
     namespace: string;
     appName: string;
   };
+  clusterIngressIp: string;
 }
 
-const WebTabs: React.FC<Props> = ({ index, service, maxRAM, maxCPU, clusterContainsGPUNodes, internalNetworkingDetails }) => {
+const WebTabs: React.FC<Props> = ({ 
+  index, 
+  service, 
+  maxRAM, 
+  maxCPU, 
+  clusterContainsGPUNodes, 
+  internalNetworkingDetails, 
+  clusterIngressIp,
+}) => {
   const [currentTab, setCurrentTab] = React.useState<
     "main" | "resources" | "networking" | "advanced"
   >("main");
@@ -49,6 +58,7 @@ const WebTabs: React.FC<Props> = ({ index, service, maxRAM, maxCPU, clusterConta
             index={index}
             service={service}
             internalNetworkingDetails={internalNetworkingDetails}
+            clusterIngressIp={clusterIngressIp}
           />
         ))
         .with("resources", () => (

+ 7 - 0
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/utils.ts

@@ -40,6 +40,13 @@ export const AWS_INSTANCE_LIMITS: InstanceTypes = Object.freeze({
     xlarge: { vCPU: 4, RAM: 16 },
     "2xlarge": { vCPU: 8, RAM: 32 },
   },
+  c6a: {
+    large: { vCPU: 2, RAM: 4 },
+    xlarge: { vCPU: 4, RAM: 8 },
+    "2xlarge": { vCPU: 8, RAM: 16 },
+    "4xlarge": { vCPU: 16, RAM: 32 },
+    "8xlarge": { vCPU: 32, RAM: 64 },
+  },
   c6i: {
     large: { vCPU: 2, RAM: 4 },
     xlarge: { vCPU: 4, RAM: 8 },

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

@@ -279,11 +279,6 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
               <ServiceList
                 addNewText={"Add a new service"}
                 fieldArrayName={"app.services"}
-                maxCPU={currentClusterResources.maxCPU}
-                maxRAM={currentClusterResources.maxRAM}
-                clusterContainsGPUNodes={
-                  currentClusterResources.clusterContainsGPUNodes
-                }
                 internalNetworkingDetails={{
                   namespace: deploymentTarget.namespace,
                   appName: porterApp.name,
@@ -322,11 +317,6 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
                 }
                 isPredeploy
                 fieldArrayName={"app.predeploy"}
-                maxCPU={currentClusterResources.maxCPU}
-                maxRAM={currentClusterResources.maxRAM}
-                clusterContainsGPUNodes={
-                  currentClusterResources.clusterContainsGPUNodes
-                }
               />
             </>,
             <Button

+ 1 - 1
dashboard/src/main/home/sidebar/ClusterList.tsx

@@ -120,7 +120,7 @@ const ClusterList: React.FC<PropsType> = (props) => {
             {(clusters.length > 1 || user.isPorterUser) && <i className="material-icons">arrow_drop_down</i>}
           </NavButton>
         </MainSelector>
-        {(clusters.length > 1 || user.isPorterUser) && renderDropdown()}
+        {(clusters.length > 1) && renderDropdown()}
         {
           clusterModalVisible && <ProvisionClusterModal
             closeModal={() => setClusterModalVisible(false)} />

+ 3 - 1
dashboard/src/shared/ClusterResourcesContext.tsx

@@ -9,6 +9,7 @@ export type ClusterResources = {
   defaultCPU: number;
   defaultRAM: number;
   clusterContainsGPUNodes: boolean;
+  clusterIngressIp: string;
 };
 
 export const ClusterResourcesContext = createContext<{
@@ -28,7 +29,7 @@ export const useClusterResources = () => {
 const ClusterResourcesProvider = ({ children }: { children: JSX.Element }) => {
   const { currentCluster, currentProject } = useContext(Context);
 
-  const { maxCPU, maxRAM, defaultCPU, defaultRAM, clusterContainsGPUNodes } = useClusterResourceLimits({
+  const { maxCPU, maxRAM, defaultCPU, defaultRAM, clusterContainsGPUNodes, clusterIngressIp } = useClusterResourceLimits({
     projectId: currentProject?.id,
     clusterId: currentCluster?.id,
   });
@@ -42,6 +43,7 @@ const ClusterResourcesProvider = ({ children }: { children: JSX.Element }) => {
           defaultCPU,
           defaultRAM,
           clusterContainsGPUNodes,
+          clusterIngressIp,
         },
       }}
     >

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

@@ -1317,6 +1317,15 @@ const getImageTags = baseApi<
   return `/api/projects/${pathParams.project_id}/registries/${pathParams.registry_id}/repositories/${pathParams.repo_name}`;
 });
 
+const images = baseApi<
+  {},
+  {
+    project_id: number;
+  }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/images`;
+});
+
 const getInfra = baseApi<
   {
     version?: string;
@@ -3220,6 +3229,7 @@ export default {
   getGitRepos,
   getImageRepos,
   getImageTags,
+  images,
   listInfraTemplates,
   getInfraTemplate,
   getInfra,

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

@@ -287,6 +287,8 @@ export interface ProjectType {
   multi_cluster: boolean;
   full_add_ons: boolean;
   enable_reprovision: boolean;
+  quota_increase: boolean;
+  efs_enabled: boolean;
   validate_apply_v2: boolean;
   roles: {
     id: number;

+ 3 - 2
go.mod

@@ -83,7 +83,7 @@ require (
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.2.29
+	github.com/porter-dev/api-contracts v0.2.30
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d
@@ -96,6 +96,7 @@ require (
 	gopkg.in/yaml.v2 v2.4.0
 	gorm.io/driver/postgres v1.4.5
 	istio.io/client-go v1.16.0
+	mvdan.cc/sh/v3 v3.7.0
 )
 
 require (
@@ -344,7 +345,7 @@ require (
 	go.opencensus.io v0.24.0 // indirect
 	go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
 	golang.org/x/mod v0.8.0 // indirect
-	golang.org/x/sync v0.1.0
+	golang.org/x/sync v0.2.0
 	golang.org/x/sys v0.11.0 // indirect
 	golang.org/x/term v0.11.0 // indirect
 	golang.org/x/text v0.12.0 // indirect

+ 9 - 5
go.sum

@@ -488,8 +488,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/creack/pty v1.1.13 h1:rTPnd/xocYRjutMfqide2zle1u96upp1gm6eUHKi7us=
 github.com/creack/pty v1.1.13/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
 github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
 github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
@@ -607,7 +607,7 @@ github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
 github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8=
 github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
-github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
@@ -1522,6 +1522,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
 github.com/porter-dev/api-contracts v0.2.29 h1:K5NtjgAsVgPgJLk8/rv+UOlDHWzgHvaCqvT8gMjeFVM=
 github.com/porter-dev/api-contracts v0.2.29/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.2.30 h1:0lavqJ9zRLeDQqfZdCp1oeVlKtAqwCVnUT/X13A2wKI=
+github.com/porter-dev/api-contracts v0.2.30/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -1599,7 +1601,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY=
 github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
 github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -2137,8 +2139,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2770,6 +2772,8 @@ modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
 mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48=
 mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
 mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
+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/go.mod h1:hBpJkZE8H/sb+VRFvw2+rBpHNsTBcvSpk61hr8mzXZE=
 oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4=
 oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc=

+ 4 - 346
go.work.sum

@@ -498,12 +498,14 @@ github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzA
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
 github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c h1:Xo2rK1pzOm0jO6abTPIQwbAmqBIOj132otexc1mmzFc=
 github.com/d2g/dhcp4client v1.0.0 h1:suYBsYZIkSlUMEz4TAYCczKf62IA2UWC+O8+KtdOhCo=
 github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5 h1:+CpLbZIeUn94m02LdEKPcgErLJ347NUwxPKs5u8ieiY=
 github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4 h1:itqmmf1PFpC4n5JW+j4BU7X4MTfVurhYRTjODoPb2Y8=
 github.com/daixiang0/gci v0.2.9 h1:iwJvwQpBZmMg31w+QQ6jsyZ54KEATn6/nfARbBNW294=
 github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
+github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
 github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
 github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
 github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
@@ -838,19 +840,8 @@ github.com/porter-dev/api-contracts v0.2.27/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4
 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/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/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/pseudomuto/protoc-gen-doc v1.3.2 h1:61vWZuxYa8D7Rn4h+2dgoTNqnluBmJya2MgbqO32z6g=
 github.com/pseudomuto/protokit v0.2.0 h1:hlnBDcy3YEDXH7kc9gV+NLaN0cDzhDvD1s7Y6FZ8RpM=
@@ -859,140 +850,59 @@ 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/rules v0.0.0-20210428214800-545e0d2e0bf7 h1:cRLFDAB53r5wIkxYvtQUMnn3+B09uZTAOPmefNfVk5I=
 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/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 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/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/ryancurrah/gomodguard v1.2.3 h1:ww2fsjqocGCAFamzvv/b8IsRduuHHeK2MHTcTxZTQX8=
 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/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/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
 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/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/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/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/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
 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/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/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/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-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/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
 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/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/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/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/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
 github.com/sonatard/noctx v0.0.1 h1:VC1Qhl6Oxx9vvWo3UDgrGXYCeKCe3Wbw7qAWL6FrmTY=
 github.com/sourcegraph/go-diff v0.6.1 h1:hmA1LzxW0n1c3Q4YbrFgg4P99GSnebYa3x8gr0HZqLQ=
 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.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/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/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/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/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/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/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
 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/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/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/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/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
 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/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tomarrell/wrapcheck/v2 v2.4.0 h1:mU4H9KsqqPZUALOUbVOpjy8qNQbWLoLI9fV68/1tq30=
@@ -1008,83 +918,41 @@ 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/vt100 v0.0.0-20210615222946-8066bb97264f h1:DLpt6B5oaaS8jyXHa9VA4rrZloBVPVXeCtrOsrFauxc=
 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/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/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
 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/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 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/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/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/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/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/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/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/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/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/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/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/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/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 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/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-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/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
 github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
 github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
 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/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/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
 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/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg=
@@ -1101,182 +969,50 @@ 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.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/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 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/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/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/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/go.mod h1:ZW7vkOu9nC1CxsD8bHNHCia5JUbwP39vxgd1q4Z5rCI=
 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/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/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/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/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk=
 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/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE=
 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/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/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/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-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.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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 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.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.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 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/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/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
 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.3.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.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/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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.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.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
@@ -1287,129 +1023,55 @@ google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4q
 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/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-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-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-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-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.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/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.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/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 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/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 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/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/go.mod h1:HDvRMPQLqycKPs9nWLuzZWxsxRzISLCRORiDpBUOMqg=
 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/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
 gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
 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/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/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
 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/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
 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/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/go.mod h1:3eOeBD3Z+O/ELiuu19zynZSN8jP1ErXLuyP21SZeMq8=
 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/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/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/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/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/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
 k8s.io/metrics v0.25.2 h1:105TuPaIFfr4EHzN56WwZJO7r1UesuDytNTzeMqGySo=
 k8s.io/metrics v0.25.2/go.mod h1:4NDAauOuEJ+NWO2+hWkhFE4rWBx/plLWJOYU3vGl0sA=
 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=
 modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
 modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
@@ -1427,11 +1089,7 @@ mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7 h1:HT3e4Krq+IE44tiN36RvVEb6t
 rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
 rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
 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/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/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=

+ 1 - 0
internal/integrations/ci/actions/stack.go

@@ -177,6 +177,7 @@ func getStackApplyActionYAML(opts *GetStackApplyActionYAMLOpts) ([]byte, error)
 	gaSteps := []GithubActionYAMLStep{
 		getCheckoutCodeStep(),
 		getSetTagStep(),
+		getSetupPorterStep(),
 		getDeployStackStep(
 			opts.ServerURL,
 			opts.SecretName,

+ 9 - 5
internal/integrations/ci/actions/steps.go

@@ -26,6 +26,13 @@ func getSetTagStep() GithubActionYAMLStep {
 	}
 }
 
+func getSetupPorterStep() GithubActionYAMLStep {
+	return GithubActionYAMLStep{
+		Name: "Setup porter",
+		Uses: "porter-dev/setup-porter@v0.1.0",
+	}
+}
+
 func getUpdateAppStep(serverURL, porterTokenSecretName string, projectID uint, clusterID uint, appName string, appNamespace, actionVersion string) GithubActionYAMLStep {
 	return GithubActionYAMLStep{
 		Name: "Update Porter App",
@@ -83,7 +90,7 @@ func getDeployStackStep(
 		path = "porter.yaml"
 	}
 
-	command := fmt.Sprintf("apply -f %s", path)
+	command := fmt.Sprintf("porter apply -f %s", path)
 	if preview {
 		command = fmt.Sprintf("%s --preview", command)
 	}
@@ -95,10 +102,7 @@ func getDeployStackStep(
 
 	return GithubActionYAMLStep{
 		Name: name,
-		Uses: fmt.Sprintf("%s@%s", cliActionName, actionVersion),
-		With: map[string]string{
-			"command": command,
-		},
+		Run:  command,
 		Env: map[string]string{
 			"PORTER_CLUSTER":    fmt.Sprintf("%d", clusterID),
 			"PORTER_HOST":       serverURL,

+ 14 - 1
internal/models/project.go

@@ -28,6 +28,9 @@ const (
 	// DBEnabled enables the "Databases" tab
 	DBEnabled FeatureFlagLabel = "db_enabled"
 
+	// EFSEnabled enables the "EFS" checkbox in App Settings
+	EFSEnabled FeatureFlagLabel = "efs_enabled"
+
 	// EnableReprovision enables the provisioning button after initial creation of the cluster
 	EnableReprovision FeatureFlagLabel = "enable_reprovision"
 
@@ -49,6 +52,9 @@ const (
 	// RDSDatabasesEnabled allows for users to provision RDS instances within their cluster vpc
 	RDSDatabasesEnabled FeatureFlagLabel = "rds_databases_enabled"
 
+	// QuotaIncrease enables whether we allow for auto increase of quota_increase
+	QuotaIncrease FeatureFlagLabel = "quota_increase"
+
 	// SimplifiedViewEnabled shows the new UI dashboard or not
 	SimplifiedViewEnabled FeatureFlagLabel = "simplified_view_enabled"
 
@@ -65,6 +71,7 @@ var ProjectFeatureFlags = map[FeatureFlagLabel]bool{
 	AzureEnabled:           false,
 	CapiProvisionerEnabled: true,
 	DBEnabled:              false,
+	EFSEnabled:             false,
 	EnableReprovision:      false,
 	FullAddOns:             false,
 	HelmValuesEnabled:      false,
@@ -72,6 +79,7 @@ var ProjectFeatureFlags = map[FeatureFlagLabel]bool{
 	MultiCluster:           false,
 	PreviewEnvsEnabled:     false,
 	RDSDatabasesEnabled:    false,
+	QuotaIncrease:          false,
 	SimplifiedViewEnabled:  true,
 	StacksEnabled:          false,
 	ValidateApplyV2:        true,
@@ -195,6 +203,8 @@ func (p *Project) GetFeatureFlag(flagName FeatureFlagLabel, launchDarklyClient *
 			return p.MultiCluster
 		case "preview_envs_enabled":
 			return p.PreviewEnvsEnabled
+		case "quota_increase":
+			return false
 		case "rds_databases_enabled":
 			return p.RDSDatabasesEnabled
 		case "simplified_view_enabled":
@@ -203,6 +213,8 @@ func (p *Project) GetFeatureFlag(flagName FeatureFlagLabel, launchDarklyClient *
 			return p.StacksEnabled
 		case "validate_apply_v2":
 			return p.ValidateApplyV2
+		case "efs_enabled":
+			return false
 		}
 	}
 
@@ -225,7 +237,6 @@ func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Proje
 
 	projectID := p.ID
 	projectName := p.Name
-
 	return types.Project{
 		ID:    projectID,
 		Name:  projectName,
@@ -245,6 +256,8 @@ func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Proje
 		EnableReprovision:      p.GetFeatureFlag(EnableReprovision, launchDarklyClient),
 		ValidateApplyV2:        p.GetFeatureFlag(ValidateApplyV2, launchDarklyClient),
 		FullAddOns:             p.GetFeatureFlag(FullAddOns, launchDarklyClient),
+		QuotaIncrease:          p.GetFeatureFlag(QuotaIncrease, launchDarklyClient),
+		EFSEnabled:             p.GetFeatureFlag(EFSEnabled, launchDarklyClient),
 	}
 }
 

+ 19 - 2
internal/porter_app/v2/yaml.go

@@ -88,8 +88,9 @@ type PorterApp struct {
 	Build    *Build            `yaml:"build,omitempty"`
 	Env      map[string]string `yaml:"env,omitempty"`
 
-	Predeploy *Service   `yaml:"predeploy,omitempty"`
-	EnvGroups []EnvGroup `yaml:"envGroups,omitempty"`
+	Predeploy  *Service    `yaml:"predeploy,omitempty"`
+	EnvGroups  []EnvGroup  `yaml:"envGroups,omitempty"`
+	EfsStorage *EfsStorage `yaml:"efsStorage,omitempty"`
 }
 
 // PorterYAML represents all the possible fields in a Porter YAML file
@@ -98,6 +99,11 @@ type PorterYAML struct {
 	Previews  *PorterApp `yaml:"previews,omitempty"`
 }
 
+// EfsStorage represents the EFS storage settings for a Porter app
+type EfsStorage struct {
+	Enabled bool `yaml:"enabled"`
+}
+
 // Build represents the build settings for a Porter app
 type Build struct {
 	Context    string   `yaml:"context" validate:"dir"`
@@ -227,6 +233,11 @@ func ProtoFromApp(ctx context.Context, porterApp PorterApp) (*porterv1.PorterApp
 	}
 	appProto.EnvGroups = envGroups
 
+	if porterApp.EfsStorage != nil {
+		appProto.EfsStorage = &porterv1.EFS{
+			Enabled: porterApp.EfsStorage.Enabled,
+		}
+	}
 	return appProto, porterApp.Env, nil
 }
 
@@ -403,6 +414,12 @@ func AppFromProto(appProto *porterv1.PorterApp) (PorterApp, error) {
 		})
 	}
 
+	if appProto.EfsStorage != nil {
+		porterApp.EfsStorage = &EfsStorage{
+			Enabled: appProto.EfsStorage.Enabled,
+		}
+	}
+
 	return porterApp, nil
 }
 

+ 9 - 0
services/porter_cli_container/Dockerfile

@@ -4,6 +4,15 @@ COPY get-porter-cli.sh /scratch/
 
 RUN apt-get update && apt-get install -y curl unzip git
 
+ARG DOCKER_VERSION=24.0.6
+ARG DOCKER_BUILDX_VERSION=0.11.2
+
+RUN curl -fsSLO https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz && \
+    tar --strip-components=1 -xvzf docker-${DOCKER_VERSION}.tgz -C /usr/local/bin && \
+    mkdir -p /usr/local/libexec/docker/cli-plugins && \
+    curl -fsSLo /usr/local/libexec/docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v${DOCKER_BUILDX_VERSION}/buildx-v${DOCKER_BUILDX_VERSION}.linux-amd64 && \
+    chmod +x /usr/local/libexec/docker/cli-plugins/docker-buildx
+
 ARG VERSION
 
 RUN /scratch/get-porter-cli.sh