Procházet zdrojové kódy

Merge branch 'master' into sms/helm-convert

Stefan McShane před 2 roky
rodič
revize
f11b428254
65 změnil soubory, kde provedl 2718 přidání a 225 odebrání
  1. 21 1
      .github/workflows/production.yml
  2. 1 7
      api/server/handlers/gitinstallation/get_porter_yaml.go
  3. 156 0
      api/server/handlers/porter_app/app_metrics.go
  4. 3 2
      api/server/handlers/porter_app/parse_yaml.go
  5. 1 1
      api/server/handlers/project/create.go
  6. 1 1
      api/server/handlers/project/delete.go
  7. 1 1
      api/server/handlers/project/get.go
  8. 4 3
      api/server/handlers/project/get_test.go
  9. 1 1
      api/server/handlers/project/rename.go
  10. 29 0
      api/server/router/porter_app.go
  11. 11 9
      api/server/shared/apitest/config.go
  12. 3 0
      api/server/shared/apitest/response.go
  13. 4 0
      api/server/shared/config/config.go
  14. 2 0
      api/server/shared/config/env/envconfs.go
  15. 7 0
      api/server/shared/config/loader/loader.go
  16. 7 7
      dashboard/package-lock.json
  17. 1 1
      dashboard/package.json
  18. 18 9
      dashboard/src/components/porter/VerticalSteps.tsx
  19. 2 1
      dashboard/src/lib/hooks/useDeploymentTarget.ts
  20. 66 0
      dashboard/src/lib/hooks/useRevisionList.ts
  21. 23 11
      dashboard/src/lib/porter-apps/index.ts
  22. 1 0
      dashboard/src/lib/revisions/types.ts
  23. 40 20
      dashboard/src/main/home/Home.tsx
  24. 10 3
      dashboard/src/main/home/add-on-dashboard/AddOnDashboard.tsx
  25. 10 8
      dashboard/src/main/home/add-on-dashboard/NewAddOnFlow.tsx
  26. 10 3
      dashboard/src/main/home/app-dashboard/AppDashboard.tsx
  27. 11 8
      dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx
  28. 3 3
      dashboard/src/main/home/app-dashboard/app-view/RevisionsList.tsx
  29. 2 1
      dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx
  30. 23 0
      dashboard/src/main/home/app-dashboard/app-view/tabs/MetricsTab.tsx
  31. 4 2
      dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx
  32. 13 7
      dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx
  33. 1 0
      dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts
  34. 0 2
      dashboard/src/main/home/app-dashboard/expanded-app/status/StatusSection.tsx
  35. 85 7
      dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx
  36. 47 5
      dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts
  37. 364 0
      dashboard/src/main/home/app-dashboard/validate-apply/metrics/MetricsSection.tsx
  38. 5 5
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx
  39. 9 2
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx
  40. 1 4
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx
  41. 1 1
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Health.tsx
  42. 9 8
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx
  43. 3 2
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Main.tsx
  44. 3 4
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx
  45. 4 2
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx
  46. 1 1
      dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx
  47. 6 6
      dashboard/src/main/home/sidebar/ProjectButton.tsx
  48. 0 27
      dashboard/src/main/home/sidebar/ProjectButtonContainer.tsx
  49. 6 10
      dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx
  50. 0 2
      dashboard/src/main/home/sidebar/Sidebar.tsx
  51. 22 0
      dashboard/src/shared/api.tsx
  52. 11 1
      go.mod
  53. 30 2
      go.sum
  54. 768 0
      go.work.sum
  55. 47 0
      internal/features/launch_darkly.go
  56. 1 1
      internal/kubernetes/config.go
  57. 96 0
      internal/models/feature_flag.go
  58. 25 19
      internal/models/project.go
  59. 18 3
      internal/porter_app/parse.go
  60. 77 1
      internal/porter_app/parse_test.go
  61. 3 0
      internal/porter_app/revisions.go
  62. 95 0
      internal/porter_app/testdata/v1_input_no_build_no_image.yaml
  63. 117 0
      internal/porter_app/v1/types.go
  64. 369 0
      internal/porter_app/v1/yaml.go
  65. 5 0
      zarf/helm/.serverenv

+ 21 - 1
.github/workflows/production.yml

@@ -21,7 +21,6 @@ jobs:
         uses: actions/setup-go@v4
         with:
           cache: false
-          go-version: '1.20.5'
           go-version-file: go.mod
       - name: Download Go Modules
         run: go mod download
@@ -102,3 +101,24 @@ jobs:
           project: "5"
           tag: ${{ steps.vars.outputs.sha_short }}
           token: ${{ secrets.PORTER_TOKEN_5 }}
+
+  deploy-worker-pool:
+    runs-on: ubuntu-latest
+    needs: [build-go, build-npm] # don't run this step unless these finish successfully
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+      - name: Set Github tag
+        id: vars
+        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+      - name: Update Worker Pool (revision cull job)
+        timeout-minutes: 20
+        uses: porter-dev/porter-update-action@v0.1.0
+        with:
+          app: cull-helm-revisions
+          cluster: "9"
+          host: https://dashboard.internal-tools.porter.run
+          namespace: default
+          project: "5"
+          tag: ${{ steps.vars.outputs.sha_short }}
+          token: ${{ secrets.PORTER_TOKEN_5 }}

+ 1 - 7
api/server/handlers/gitinstallation/get_porter_yaml.go

@@ -103,13 +103,7 @@ func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	}
 
 	if project.ValidateApplyV2 {
-		if parsed.Version == nil {
-			err = telemetry.Error(ctx, span, nil, "v2 porter yaml is required")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-			return
-		}
-
-		if *parsed.Version != "v2" {
+		if parsed.Version != nil && *parsed.Version != "v2" {
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 			return

+ 156 - 0
api/server/handlers/porter_app/app_metrics.go

@@ -0,0 +1,156 @@
+package porter_app
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
+
+	"connectrpc.com/connect"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/porter/internal/telemetry"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// AppMetricsHandler handles the /apps/metrics endpoint
+type AppMetricsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewAppMetricsHandler returns a new AppMetricsHandler
+func NewAppMetricsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *AppMetricsHandler {
+	return &AppMetricsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// MetricsRequest is the expected request body for the /apps/metrics endpoint
+type MetricsRequest struct {
+	// Deployment target of the app to query for metrics
+	DeploymentTargetID string `schema:"deployment_target_id"`
+
+	// Below is just a copy of prometheus.QueryOpts, other than namespace
+	// the name of the metric being queried for
+	Metric    string   `schema:"metric"`
+	ShouldSum bool     `schema:"shouldsum"`
+	Kind      string   `schema:"kind"`
+	PodList   []string `schema:"pods"`
+	Name      string   `schema:"name"`
+	// start time (in unix timestamp) for prometheus results
+	StartRange uint `schema:"startrange"`
+	// end time time (in unix timestamp) for prometheus results
+	EndRange   uint    `schema:"endrange"`
+	Resolution string  `schema:"resolution"`
+	Percentile float64 `schema:"percentile"`
+}
+
+// ServeHTTP returns metrics for a given app in the provided deployment target
+func (c *AppMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-app-metrics")
+	defer span.End()
+	r = r.Clone(ctx)
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	request := &MetricsRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if request.DeploymentTargetID == "" {
+		err := telemetry.Error(ctx, span, nil, "must provide deployment target id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID})
+
+	deploymentTargetDetailsReq := connect.NewRequest(&porterv1.DeploymentTargetDetailsRequest{
+		ProjectId:          int64(project.ID),
+		DeploymentTargetId: request.DeploymentTargetID,
+	})
+
+	deploymentTargetDetailsResp, err := c.Config().ClusterControlPlaneClient.DeploymentTargetDetails(ctx, deploymentTargetDetailsReq)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting deployment target details from cluster control plane client")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	if deploymentTargetDetailsResp == nil || deploymentTargetDetailsResp.Msg == nil {
+		err := telemetry.Error(ctx, span, err, "deployment target details resp is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	if deploymentTargetDetailsResp.Msg.ClusterId != int64(cluster.ID) {
+		err := telemetry.Error(ctx, span, err, "deployment target details resp cluster id does not match cluster id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	namespace := deploymentTargetDetailsResp.Msg.Namespace
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "namespace", Value: namespace})
+
+	agent, err := c.GetAgent(r, cluster, "")
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting k8s agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	// get prometheus service
+	promSvc, found, err := prometheus.GetPrometheusService(agent.Clientset)
+	if err != nil || !found {
+		err = telemetry.Error(ctx, span, err, "error getting prometheus service")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "metric", Value: request.Metric},
+		telemetry.AttributeKV{Key: "shouldsum", Value: request.ShouldSum},
+		telemetry.AttributeKV{Key: "kind", Value: request.Kind},
+		telemetry.AttributeKV{Key: "name", Value: request.Name},
+		telemetry.AttributeKV{Key: "start-range", Value: request.StartRange},
+		telemetry.AttributeKV{Key: "end-range", Value: request.EndRange},
+		telemetry.AttributeKV{Key: "resolution", Value: request.Resolution},
+		telemetry.AttributeKV{Key: "percentile", Value: request.Percentile},
+	)
+
+	queryOpts := &prometheus.QueryOpts{
+		Metric:     request.Metric,
+		ShouldSum:  request.ShouldSum,
+		Kind:       request.Kind,
+		Name:       request.Name,
+		Namespace:  namespace,
+		StartRange: request.StartRange,
+		EndRange:   request.EndRange,
+		Resolution: request.Resolution,
+		Percentile: request.Percentile,
+	}
+
+	rawQuery, err := prometheus.QueryPrometheus(agent.Clientset, promSvc, queryOpts)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error querying prometheus")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	c.WriteResult(w, r, rawQuery)
+}

+ 3 - 2
api/server/handlers/porter_app/parse_yaml.go

@@ -37,6 +37,7 @@ func NewParsePorterYAMLToProtoHandler(
 // ParsePorterYAMLToProtoRequest is the request object for the /apps/parse endpoint
 type ParsePorterYAMLToProtoRequest struct {
 	B64Yaml string `json:"b64_yaml"`
+	AppName string `json:"app_name"`
 }
 
 // ParsePorterYAMLToProtoResponse is the response object for the /apps/parse endpoint
@@ -53,7 +54,7 @@ func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http
 
 	if !project.ValidateApplyV2 {
 		err := telemetry.Error(ctx, span, nil, "project does not have apply v2 enabled")
-		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
 		return
 	}
 
@@ -82,7 +83,7 @@ func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
-	appProto, err := porter_app.ParseYAML(ctx, yaml)
+	appProto, err := porter_app.ParseYAML(ctx, yaml, request.AppName)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error parsing yaml")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))

+ 1 - 1
api/server/handlers/project/create.go

@@ -81,7 +81,7 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	p.WriteResult(w, r, proj.ToProjectType())
+	p.WriteResult(w, r, proj.ToProjectType(p.Config().LaunchDarklyClient))
 
 	// add project to billing team
 	_, err = p.Config().BillingManager.CreateTeam(user, proj)

+ 1 - 1
api/server/handlers/project/delete.go

@@ -87,7 +87,7 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	p.WriteResult(w, r, deletedProject.ToProjectType())
+	p.WriteResult(w, r, deletedProject.ToProjectType(p.Config().LaunchDarklyClient))
 
 	// delete the billing team
 	if err := p.Config().BillingManager.DeleteTeam(user, proj); err != nil {

+ 1 - 1
api/server/handlers/project/get.go

@@ -26,5 +26,5 @@ func NewProjectGetHandler(
 func (p *ProjectGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	p.WriteResult(w, r, proj.ToProjectType())
+	p.WriteResult(w, r, proj.ToProjectType(p.Config().LaunchDarklyClient))
 }

+ 4 - 3
api/server/handlers/project/get_test.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apitest"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/models"
 )
 
@@ -33,8 +34,8 @@ func TestGetProjectSuccessful(t *testing.T) {
 
 	handler.ServeHTTP(rr, req)
 
-	expProject := proj.ToProjectType()
-	gotProject := &types.Project{}
+	expProject := proj.ToProjectType(&features.Client{})
+	gotProject := types.Project{}
 
-	apitest.AssertResponseExpected(t, rr, expProject, gotProject)
+	apitest.AssertResponseExpected(t, rr, &expProject, &gotProject)
 }

+ 1 - 1
api/server/handlers/project/rename.go

@@ -47,5 +47,5 @@ func (c *RenameProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	c.WriteResult(w, r, project.ToProjectType())
+	c.WriteResult(w, r, project.ToProjectType(c.Config().LaunchDarklyClient))
 }

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

@@ -891,5 +891,34 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/metrics -> cluster.NewGetPodMetricsHandler
+	appMetricsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/apps/metrics",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	appMetricsHandler := porter_app.NewAppMetricsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: appMetricsEndpoint,
+		Handler:  appMetricsHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 11 - 9
api/server/shared/apitest/config.go

@@ -4,6 +4,7 @@ import (
 	"os"
 	"testing"
 
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/telemetry"
 
 	"github.com/porter-dev/porter/api/server/shared/config"
@@ -51,15 +52,16 @@ func (t *TestConfigLoader) LoadConfig() (*config.Config, error) {
 	notifier := NewFakeUserNotifier()
 
 	return &config.Config{
-		Logger:          l,
-		Repo:            repo,
-		Store:           store,
-		ServerConf:      envConf.ServerConf,
-		TokenConf:       tokenConf,
-		UserNotifier:    notifier,
-		AnalyticsClient: analytics.InitializeAnalyticsSegmentClient("", l),
-		BillingManager:  &billing.NoopBillingManager{},
-		TelemetryConfig: telemetry.TracerConfig{ServiceName: "fake", CollectorURL: "fake"},
+		Logger:             l,
+		Repo:               repo,
+		Store:              store,
+		ServerConf:         envConf.ServerConf,
+		TokenConf:          tokenConf,
+		UserNotifier:       notifier,
+		LaunchDarklyClient: &features.Client{},
+		AnalyticsClient:    analytics.InitializeAnalyticsSegmentClient("", l),
+		BillingManager:     &billing.NoopBillingManager{},
+		TelemetryConfig:    telemetry.TracerConfig{ServiceName: "fake", CollectorURL: "fake"},
 	}, nil
 }
 

+ 3 - 0
api/server/shared/apitest/response.go

@@ -10,6 +10,9 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+// AssertResponseExpected asserts that the expected http response matches the actual response
+//
+// Note that arguments need to be passed as pointer values due to how testify/assert handles serialization
 func AssertResponseExpected(t *testing.T, rr *httptest.ResponseRecorder, expResponse interface{}, gotTarget interface{}) {
 	err := json.NewDecoder(rr.Body).Decode(gotTarget)
 	if err != nil {

+ 4 - 0
api/server/shared/config/config.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/internal/analytics"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/billing"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
 	"github.com/porter-dev/porter/internal/integrations/powerdns"
 	"github.com/porter-dev/porter/internal/nats"
@@ -69,6 +70,9 @@ type Config struct {
 	// GoogleConf is the configuration for a Google OAuth client
 	GoogleConf *oauth2.Config
 
+	// LaunchDarklyClient is the client for the LaunchDarkly feature flag service
+	LaunchDarklyClient *features.Client
+
 	// SlackConf is the configuration for a Slack OAuth client
 	SlackConf *oauth2.Config
 

+ 2 - 0
api/server/shared/config/env/envconfs.go

@@ -55,6 +55,8 @@ type ServerConf struct {
 	GoogleClientSecret     string `env:"GOOGLE_CLIENT_SECRET"`
 	GoogleRestrictedDomain string `env:"GOOGLE_RESTRICTED_DOMAIN"`
 
+	LaunchDarklySDKKey string `env:"LAUNCHDARKLY_SDK_KEY"`
+
 	SendgridAPIKey                     string `env:"SENDGRID_API_KEY"`
 	SendgridPWResetTemplateID          string `env:"SENDGRID_PW_RESET_TEMPLATE_ID"`
 	SendgridPWGHTemplateID             string `env:"SENDGRID_PW_GH_TEMPLATE_ID"`

+ 7 - 0
api/server/shared/config/loader/loader.go

@@ -23,6 +23,7 @@ import (
 	"github.com/porter-dev/porter/internal/auth/sessionstore"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/billing"
+	"github.com/porter-dev/porter/internal/features"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
 	"github.com/porter-dev/porter/internal/integrations/powerdns"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -242,6 +243,12 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		sc.GithubAppSecret = append(sc.GithubAppSecret, secret...)
 	}
 
+	launchDarklyClient, err := features.GetClient(envConf)
+	if err != nil {
+		return nil, fmt.Errorf("could not create launch darkly client: %s", err)
+	}
+	res.LaunchDarklyClient = launchDarklyClient
+
 	if sc.SlackClientID != "" && sc.SlackClientSecret != "" {
 		res.Logger.Info().Msg("Creating Slack client")
 		res.SlackConf = oauth.NewSlackClient(&oauth.Config{

+ 7 - 7
dashboard/package-lock.json

@@ -13,7 +13,7 @@
         "@loadable/component": "^5.15.2",
         "@material-ui/core": "^4.11.3",
         "@material-ui/lab": "^4.0.0-alpha.61",
-        "@porter-dev/api-contracts": "^0.0.95",
+        "@porter-dev/api-contracts": "^0.0.99",
         "@react-spring/web": "^9.6.1",
         "@sentry/react": "^6.13.2",
         "@sentry/tracing": "^6.13.2",
@@ -2454,9 +2454,9 @@
       }
     },
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.0.95",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.95.tgz",
-      "integrity": "sha512-nwbpfyv5qvhjKdHU7fnR3S6+E9ijwm3/OtZ+WCItn1JZNrDZtb2x047AkBndVU6NKDtUnxHYGYwQJo5spAw7cQ==",
+      "version": "0.0.99",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.99.tgz",
+      "integrity": "sha512-boropiMEHIXJLTKxmO6689GhIMiTC95JMkL1ouFxn2mkiT6DPcJ08UfD5tKohUMYGhgQNJceBQ1biPVjn5nqJQ==",
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
       }
@@ -16943,9 +16943,9 @@
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "@porter-dev/api-contracts": {
-      "version": "0.0.95",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.95.tgz",
-      "integrity": "sha512-nwbpfyv5qvhjKdHU7fnR3S6+E9ijwm3/OtZ+WCItn1JZNrDZtb2x047AkBndVU6NKDtUnxHYGYwQJo5spAw7cQ==",
+      "version": "0.0.99",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.99.tgz",
+      "integrity": "sha512-boropiMEHIXJLTKxmO6689GhIMiTC95JMkL1ouFxn2mkiT6DPcJ08UfD5tKohUMYGhgQNJceBQ1biPVjn5nqJQ==",
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"
       }

+ 1 - 1
dashboard/package.json

@@ -8,7 +8,7 @@
     "@loadable/component": "^5.15.2",
     "@material-ui/core": "^4.11.3",
     "@material-ui/lab": "^4.0.0-alpha.61",
-    "@porter-dev/api-contracts": "^0.0.95",
+    "@porter-dev/api-contracts": "^0.0.99",
     "@react-spring/web": "^9.6.1",
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",

+ 18 - 9
dashboard/src/components/porter/VerticalSteps.tsx

@@ -22,9 +22,9 @@ const VerticalSteps: React.FC<Props> = ({
                 <Line isActive={i + 1 <= currentStep} />
               )
             }
-            <Dot
-              isActive={i <= currentStep}
-            />
+            <Dot isActive={i <= currentStep}>
+              <Number>{i+1}</Number>
+            </Dot>
             <OpacityWrapper isActive={i <= currentStep}>
               {step}
               {
@@ -42,6 +42,11 @@ const VerticalSteps: React.FC<Props> = ({
 
 export default VerticalSteps;
 
+const Number = styled.div`
+  font-size: 12px;
+  color: #fff;
+`;
+
 const ReadOnlyOverlay = styled.div`
   position: absolute;
   width: 100%;
@@ -56,7 +61,7 @@ const Line = styled.div<{
 }>`
   width: 1px;
   height: calc(100% + 35px);
-  background: ${props => props.isActive ? "#fff" : "#414141"};
+  background: #414141;
   position: absolute;
   left: 4px;
   top: 8px;
@@ -66,14 +71,18 @@ const Line = styled.div<{
 const Dot = styled.div<{
   isActive: boolean;
 }>`
-  width: 9px;
-  height: 9px;
-  background: ${props => props.isActive ? "#fff" : "#414141"};
+  width: 31px;
+  height: 31px;
+  background: ${props => props.isActive ? "#3D48C3" : "#121212"};
   border-radius: 50%;
   position: absolute;
-  left: 0;
-  top: 7px;
+  left: -11px;
+  top: -3px;
   opacity: 1;
+  border: 6px solid #121212;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 `;
 
 const OpacityWrapper = styled.div<{

+ 2 - 1
dashboard/src/lib/hooks/useDeploymentTarget.ts

@@ -19,7 +19,8 @@ export function useDefaultDeploymentTarget() {
   const { data } = useQuery(
     ["getDefaultDeploymentTarget", currentProject?.id, currentCluster?.id],
     async () => {
-      if (!currentProject?.id || !currentCluster?.id) {
+      // see Context.tsx L98 for why the last check is necessary
+      if (!currentProject?.id || !currentCluster?.id || currentCluster.id === -1) {
         return;
       }
       const res = await api.getDefaultDeploymentTarget(

+ 66 - 0
dashboard/src/lib/hooks/useRevisionList.ts

@@ -0,0 +1,66 @@
+import { useQuery } from "@tanstack/react-query";
+import { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { z } from "zod";
+import {AppRevision, appRevisionValidator} from "../revisions/types";
+import {useLatestRevision} from "../../main/home/app-dashboard/app-view/LatestRevisionContext";
+
+export function useRevisionList(appName: string, deploymentTargetId: string) {
+  const { currentProject, currentCluster } = useContext(Context);
+  const {latestRevision} = useLatestRevision();
+
+  const [
+    revisionList,
+    setRevisionList,
+  ] = useState<AppRevision[]>([]);
+
+  if (currentProject == null || currentCluster == null) {
+    return [];
+  }
+
+  const {data} = useQuery(
+      ["listAppRevisions", currentProject.id, currentCluster.id, appName, deploymentTargetId, latestRevision],
+      async () => {
+        const res = await api.listAppRevisions(
+            "<token>",
+            {
+              deployment_target_id: deploymentTargetId,
+            },
+            {
+              project_id: currentProject.id,
+              cluster_id: currentCluster.id,
+              porter_app_name: appName,
+            }
+        );
+
+        const revisions = await z
+            .object({
+              app_revisions: z.array(appRevisionValidator),
+            })
+            .parseAsync(res.data);
+
+        return revisions;
+      }
+  );
+
+  useEffect(() => {
+    if (data) {
+      setRevisionList(data.app_revisions);
+    }
+  }, [data]);
+
+  return revisionList;
+}
+
+export function useRevisionIdToNumber(appName: string, deploymentTargetId: string) {
+    const revisionList = useRevisionList(appName, deploymentTargetId);
+    const revisionIdToNumber: Record<string, number> = Object.fromEntries(revisionList.map(r => ([r.id, r.revision_number ])))
+
+    return revisionIdToNumber;
+}
+
+export function useLatestRevisionNumber(appName: string, deploymentTargetId: string) {
+  const revisionList = useRevisionList(appName, deploymentTargetId);
+  return revisionList.map((revision) => revision.revision_number).reduce((a, b) => Math.max(a, b), 0)
+}

+ 23 - 11
dashboard/src/lib/porter-apps/index.ts

@@ -68,6 +68,7 @@ export const deletionValidator = z.object({
 export const clientAppValidator = z.object({
   name: z.string().min(1),
   services: serviceValidator.array(),
+  predeploy: serviceValidator.array().optional(),
   env: z.record(z.string(), z.string()).default({}),
   build: buildValidator,
 });
@@ -164,13 +165,12 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
   const { app, source } = data;
 
   const services = app.services
-    .filter((s) => !isPredeployService(s))
     .reduce((acc: Record<string, Service>, svc) => {
       acc[svc.name.value] = serviceProto(serializeService(svc));
       return acc;
     }, {});
 
-  const predeploy = app.services.find((s) => isPredeployService(s));
+  const predeploy = app.predeploy?.[0]
 
   const proto = match(source)
     .with(
@@ -273,10 +273,21 @@ export function clientAppFromProto(
       return deserializeService({ service: svc });
     });
 
+  const predeployList = [];
+  if (proto.predeploy) {
+    predeployList.push(deserializeService({
+      service: serializedServiceFromProto({
+        name: "pre-deploy",
+        service: proto.predeploy,
+        isPredeploy: true,
+      })
+    }))
+  }
   if (!overrides?.predeploy) {
     return {
       name: proto.name,
       services,
+      predeploy: predeployList,
       env: proto.env,
       build: clientBuildFromProto(proto.build) ?? {
         method: "pack",
@@ -289,19 +300,20 @@ export function clientAppFromProto(
 
   const predeployOverrides = serializeService(overrides.predeploy);
   const predeploy = proto.predeploy
-    ? deserializeService({
-        service: serializedServiceFromProto({
-          name: "pre-deploy",
-          service: proto.predeploy,
-          isPredeploy: true,
-        }),
-        override: predeployOverrides,
-      })
+    ? [deserializeService({
+      service: serializedServiceFromProto({
+        name: "pre-deploy",
+        service: proto.predeploy,
+        isPredeploy: true,
+      }),
+      override: predeployOverrides,
+    })]
     : undefined;
 
   return {
     name: proto.name,
-    services: [...services, predeploy].filter(valueExists),
+    services,
+    predeploy,
     env: proto.env,
     build: clientBuildFromProto(proto.build) ?? {
       method: "pack",

+ 1 - 0
dashboard/src/lib/revisions/types.ts

@@ -13,6 +13,7 @@ export const appRevisionValidator = z.object({
   ]),
   b64_app_proto: z.string(),
   revision_number: z.number(),
+  id: z.string(),
   created_at: z.string(),
   updated_at: z.string(),
 });

+ 40 - 20
dashboard/src/main/home/Home.tsx

@@ -134,15 +134,35 @@ const Home: React.FC<Props> = (props) => {
       } else if (projectList.length > 0 && !currentProject) {
         setProjects(projectList);
 
-        id = id ?? Number(localStorage.getItem("currentProject"));
-        const foundProjectListEntry = projectList.find(
-          (item: ProjectListType) => item.id === id
-        );
+        let foundProject = null;
+        if (id) {
+          projectList.forEach((project: ProjectListType, i: number) => {
+            if (project.id === id) {
+              foundProject = project;
+            }
+          });
 
-        const project = await api
-          .getProject("<token>", {}, { id: foundProjectListEntry?.id || projectList[0].id })
-          .then((res) => res.data as ProjectType);
-        setCurrentProject(project);
+          const project = await api
+            .getProject("<token>", {}, { id: projectList[0].id })
+            .then((res) => res.data as ProjectType);
+
+          setCurrentProject(foundProject || project);
+        }
+        if (!foundProject) {
+          projectList.forEach((project: ProjectListType, i: number) => {
+            if (
+              project.id.toString() ===
+              localStorage.getItem("currentProject")
+            ) {
+              foundProject = project;
+            }
+          });
+          const project = await api
+            .getProject("<token>", {}, { id: projectList[0].id })
+            .then((res) => res.data as ProjectType);
+
+          setCurrentProject(foundProject || project);
+        }
       }
     } catch (error) {
       console.log(error);
@@ -183,7 +203,7 @@ const Home: React.FC<Props> = (props) => {
       } else {
         setHasFinishedOnboarding(true);
       }
-    } catch (error) {}
+    } catch (error) { }
   };
 
   useEffect(() => {
@@ -457,17 +477,17 @@ const Home: React.FC<Props> = (props) => {
               overrideInfraTabEnabled({
                 projectID: currentProject?.id,
               })) && (
-              <Route
-                path="/infrastructure"
-                render={() => {
-                  return (
-                    <DashboardWrapper>
-                      <InfrastructureRouter />
-                    </DashboardWrapper>
-                  );
-                }}
-              />
-            )}
+                <Route
+                  path="/infrastructure"
+                  render={() => {
+                    return (
+                      <DashboardWrapper>
+                        <InfrastructureRouter />
+                      </DashboardWrapper>
+                    );
+                  }}
+                />
+              )}
             <Route
               path="/dashboard"
               render={() => {

+ 10 - 3
dashboard/src/main/home/add-on-dashboard/AddOnDashboard.tsx

@@ -155,8 +155,7 @@ const AddOnDashboard: React.FC<Props> = ({
         <ClusterProvisioningPlaceholder />
       ) : (
 
-
-        (filteredAddOns.length === 0) ? (
+        (addOns.length === 0) ? (
 
           isLoading ?
             (<Loading offset="-150px" />) : (
@@ -209,7 +208,14 @@ const AddOnDashboard: React.FC<Props> = ({
             </Container>
             <Spacer y={1} />
 
-            {isLoading ? <Loading offset="-150px" /> : view === "grid" ? (
+            {filteredAddOns.length === 0 ? (
+              <Fieldset>
+                <Container row>
+                  <PlaceholderIcon src={notFound} />
+                  <Text color="helper">No matching add-ons were found.</Text>
+                </Container>
+              </Fieldset>
+            ) : (isLoading ? <Loading offset="-150px" /> : view === "grid" ? (
               <GridList>
                 {(filteredAddOns ?? []).map((app: any, i: number) => {
                   return (
@@ -261,6 +267,7 @@ const AddOnDashboard: React.FC<Props> = ({
                   );
                 })}
               </List>
+            )
             )}
           </>
         ))}

+ 10 - 8
dashboard/src/main/home/add-on-dashboard/NewAddOnFlow.tsx

@@ -192,11 +192,12 @@ const NewAddOnFlow: React.FC<Props> = ({
                           <div>
                             <Text color="helper">For developer productivity.</Text>
                           </div>
+                          <TemplateList
+                            templates={appTemplates} // This is where you provide only APP templates
+                            setCurrentTemplate={(x) => setCurrentTemplate(x)}
+                          />
                         </>}
-                      <TemplateList
-                        templates={appTemplates} // This is where you provide only APP templates
-                        setCurrentTemplate={(x) => setCurrentTemplate(x)}
-                      />
+
                       {dataStoreTemplates?.length > 0 &&
                         <>
                           <div>
@@ -205,11 +206,12 @@ const NewAddOnFlow: React.FC<Props> = ({
                           <div>
                             <Text color="helper">Pre-production datastores are not highly available and use ephemeral storage.</Text>
                           </div>
+                          <TemplateList
+                            templates={dataStoreTemplates} // This is where you provide only DATA_STORE templates
+                            setCurrentTemplate={(x) => setCurrentTemplate(x)}
+                          />
                         </>}
-                      <TemplateList
-                        templates={dataStoreTemplates} // This is where you provide only DATA_STORE templates
-                        setCurrentTemplate={(x) => setCurrentTemplate(x)}
-                      />
+
 
                       {filteredTemplates?.length > 0 && (currentProject?.full_add_ons || user.isPorterUser) &&
                         <>

+ 10 - 3
dashboard/src/main/home/app-dashboard/AppDashboard.tsx

@@ -215,7 +215,7 @@ const AppDashboard: React.FC<Props> = ({ }) => {
       {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
         <ClusterProvisioningPlaceholder />
       ) : (
-        filteredApps.length === 0 ? (
+        apps.length === 0 ? (
           isLoading ?
             (<Loading offset="-150px" />) : (
               <Fieldset>
@@ -283,7 +283,14 @@ const AppDashboard: React.FC<Props> = ({ }) => {
             </Container>
             <Spacer y={1} />
 
-            {isLoading ? (
+            {filteredApps.length === 0 ? (
+              <Fieldset>
+                <Container row>
+                  <PlaceholderIcon src={notFound} />
+                  <Text color="helper">No matching apps were found.</Text>
+                </Container>
+              </Fieldset>
+            ) : (isLoading ? (
               <Loading offset="-150px" />
             ) : view === "grid" ? (
               <GridList>
@@ -342,7 +349,7 @@ const AppDashboard: React.FC<Props> = ({ }) => {
                   }
                 })}
               </List>
-            )}
+            ))}
           </>
         )
       )

+ 11 - 8
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -26,6 +26,7 @@ import Button from "components/porter/Button";
 import Icon from "components/porter/Icon";
 import save from "assets/save-01.svg";
 import LogsTab from "./tabs/LogsTab";
+import MetricsTab from "./tabs/MetricsTab";
 
 // commented out tabs are not yet implemented
 // will be included as support is available based on data from app revisions rather than helm releases
@@ -34,7 +35,7 @@ const validTabs = [
   // "events",
   "overview",
   "logs",
-  // "metrics",
+  "metrics",
   // "debug",
   "environment",
   "build-settings",
@@ -108,6 +109,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
       },
     },
   });
+
   const {
     reset,
     handleSubmit,
@@ -190,7 +192,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
         porterApp.name,
       ]);
       setPreviewRevision(null);
-    } catch (err) {}
+    } catch (err) { }
   });
 
   useEffect(() => {
@@ -217,7 +219,6 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
           latestSource={latestSource}
           onSubmit={onSubmit}
         />
-        <Spacer y={1} />
         <AnimateHeight height={isDirty && !onlyExpandedChanged ? "auto" : 0}>
           <Banner
             type="warning"
@@ -247,14 +248,15 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
           options={[
             { label: "Overview", value: "overview" },
             { label: "Logs", value: "logs" },
+            { label: "Metrics", value: "metrics" },
             { label: "Environment", value: "environment" },
             ...(latestProto.build
               ? [
-                  {
-                    label: "Build Settings",
-                    value: "build-settings",
-                  },
-                ]
+                {
+                  label: "Build Settings",
+                  value: "build-settings",
+                },
+              ]
               : []),
             { label: "Settings", value: "settings" },
           ]}
@@ -275,6 +277,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
           .with("environment", () => <Environment />)
           .with("settings", () => <Settings />)
           .with("logs", () => <LogsTab />)
+          .with("metrics", () => <MetricsTab />)
           .otherwise(() => null)}
         <Spacer y={2} />
       </form>

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

@@ -390,7 +390,7 @@ const RevisionHeader = styled.div`
     cursor: pointer;
     border-radius: 20px;
     transform: ${(props: { showRevisions: boolean; isCurrent: boolean }) =>
-      props.showRevisions ? "" : "rotate(-90deg)"};
+    props.showRevisions ? "" : "rotate(-90deg)"};
     transition: transform 0.1s ease;
   }
 `;
@@ -431,7 +431,7 @@ const Tr = styled.tr`
     props.selected ? "#ffffff11" : ""};
   :hover {
     background: ${(props: { disableHover?: boolean; selected?: boolean }) =>
-      props.disableHover ? "" : "#ffffff22"};
+    props.disableHover ? "" : "#ffffff22"};
   }
 `;
 
@@ -463,7 +463,7 @@ const RollbackButton = styled.div`
     props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
     background: ${(props: { disabled: boolean }) =>
-      props.disabled ? "" : "#405eddbb"};
+    props.disabled ? "" : "#405eddbb"};
   }
 `;
 

+ 2 - 1
dashboard/src/main/home/app-dashboard/app-view/tabs/LogsTab.tsx

@@ -14,7 +14,7 @@ import Button from "components/porter/Button";
 import { useLatestRevision } from "../LatestRevisionContext";
 
 const LogsTab: React.FC = () => {
-    const { projectId, clusterId, latestProto , deploymentTargetId} = useLatestRevision();
+    const { projectId, clusterId, latestProto , deploymentTargetId, latestRevision} = useLatestRevision();
 
     const appName = latestProto.name
     const serviceNames = Object.keys(latestProto.services)
@@ -27,6 +27,7 @@ const LogsTab: React.FC = () => {
                 appName={appName}
                 serviceNames={serviceNames}
                 deploymentTargetId={deploymentTargetId}
+                latestRevision={latestRevision}
             />
         </>
     );

+ 23 - 0
dashboard/src/main/home/app-dashboard/app-view/tabs/MetricsTab.tsx

@@ -0,0 +1,23 @@
+import React from "react";
+import { useLatestRevision } from "../LatestRevisionContext";
+import MetricsSection from "../../validate-apply/metrics/MetricsSection";
+
+const MetricsTab: React.FC = () => {
+    const { projectId, clusterId, latestProto , deploymentTargetId} = useLatestRevision();
+
+    const appName = latestProto.name
+
+    return (
+        <>
+            <MetricsSection
+                projectId={projectId}
+                clusterId={clusterId}
+                appName={appName}
+                services={latestProto.services}
+                deploymentTargetId={deploymentTargetId}
+            />
+        </>
+    );
+};
+
+export default MetricsTab;

+ 4 - 2
dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx

@@ -15,7 +15,7 @@ import { useLatestRevision } from "../LatestRevisionContext";
 
 const Overview: React.FC = () => {
   const { formState } = useFormContext<PorterAppFormData>();
-  const { porterApp } = useLatestRevision();
+  const { porterApp, latestProto } = useLatestRevision();
 
   const buttonStatus = useMemo(() => {
     if (formState.isSubmitting) {
@@ -43,14 +43,16 @@ const Overview: React.FC = () => {
                 type: "predeploy",
               }),
             })}
+            existingServiceNames={Object.keys(latestProto.services)}
             isPredeploy
+            fieldArrayName={"app.predeploy"}
           />
           <Spacer y={0.5} />
         </>
       )}
       <Text size={16}>Application services</Text>
       <Spacer y={0.5} />
-      <ServiceList addNewText={"Add a new service"} />
+      <ServiceList addNewText={"Add a new service"} fieldArrayName={"app.services"} existingServiceNames={Object.keys(latestProto.services)} />
       <Spacer y={0.75} />
       <Button
         type="submit"

+ 13 - 7
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -121,6 +121,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
     setValue,
     handleSubmit,
     setError,
+    clearErrors,
     formState: { isSubmitting: isValidating, errors },
   } = porterAppFormMethods;
 
@@ -143,7 +144,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
       const validatedAppProto = await validateApp(data);
       setValidatedAppProto(validatedAppProto);
 
-      if (source?.type === "github") {
+      if (source.type === "github") {
         setShowGHAModal(true);
         return;
       }
@@ -302,6 +303,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
   // reset services when source changes
   useEffect(() => {
     setValue("app.services", []);
+    setValue("app.predeploy", []);
     setDetectedServices({
       detected: false,
       count: 0,
@@ -325,7 +327,8 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
   useEffect(() => {
     if (servicesFromYaml && !detectedServices.detected) {
       const { services, predeploy } = servicesFromYaml;
-      setValue("app.services", [...services, predeploy].filter(valueExists));
+      setValue("app.services", services);
+      setValue("app.predeploy", [predeploy].filter(valueExists));
       setDetectedServices({
         detected: true,
         count: services.length,
@@ -334,6 +337,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
 
     if (!servicesFromYaml && detectedServices.detected) {
       setValue("app.services", []);
+      setValue("app.predeploy", []);
       setDetectedServices({
         detected: false,
         count: 0,
@@ -346,7 +350,8 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
       setError("app.name", {
         message: "An app with this name already exists",
       });
-      return;
+    } else {
+      clearErrors("app.name");
     }
   }, [porterApps, name]);
 
@@ -447,16 +452,15 @@ 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>
                       )}
                     </Container>
                     <Spacer y={0.5} />
-                    <ServiceList addNewText={"Add a new service"} />
+                    <ServiceList addNewText={"Add a new service"} fieldArrayName={"app.services"} />
                   </>,
                   <>
                     <Text size={16}>Environment variables (optional)</Text>
@@ -483,8 +487,10 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                             name: "pre-deploy",
                             type: "predeploy",
                           }),
+                          expanded: true,
                         })}
                         isPredeploy
+                        fieldArrayName={"app.predeploy"}
                       />
                     </>
                   ),

+ 1 - 0
dashboard/src/main/home/app-dashboard/expanded-app/logs/types.ts

@@ -18,6 +18,7 @@ export interface PaginationInfo {
     nextCursor: string | null;
 }
 
+
 const rawLabelsValidator = z.object({
     porter_run_absolute_name: z.string().optional(),
     porter_run_app_id: z.string().optional(),

+ 0 - 2
dashboard/src/main/home/app-dashboard/expanded-app/status/StatusSection.tsx

@@ -136,8 +136,6 @@ const StatusSectionFC: React.FunctionComponent<Props> = ({
 
   return (
     <>
-      <Banner type="info">An improved debugging view is under construction. Unable to debug your application? <MyLink id={"intercom_help"}>Contact us</MyLink></Banner>
-      <Spacer y={1} />
       <StyledStatusSection>
         {renderStatusSection()}
       </StyledStatusSection>

+ 85 - 7
dashboard/src/main/home/app-dashboard/validate-apply/logs/Logs.tsx

@@ -26,6 +26,9 @@ import Button from "components/porter/Button";
 import { Service } from "../../new-app-flow/serviceTypes";
 import LogFilterContainer from "../../expanded-app/logs/LogFilterContainer";
 import StyledLogs from "../../expanded-app/logs/StyledLogs";
+import {z} from "zod";
+import {AppRevision, appRevisionValidator} from "lib/revisions/types";
+import {useLatestRevisionNumber, useRevisionIdToNumber} from "lib/hooks/useRevisionList";
 
 type Props = {
     projectId: number;
@@ -33,6 +36,7 @@ type Props = {
     appName: string;
     serviceNames: string[];
     deploymentTargetId: string;
+    latestRevision: AppRevision;
 };
 
 const Logs: React.FC<Props> = ({
@@ -41,6 +45,7 @@ const Logs: React.FC<Props> = ({
     appName,
     serviceNames,
     deploymentTargetId,
+    latestRevision,
 }) => {
     const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
     const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
@@ -56,11 +61,14 @@ const Logs: React.FC<Props> = ({
 
     const [selectedFilterValues, setSelectedFilterValues] = useState<Record<LogFilterName, string>>({
         service_name:  GenericLogFilter.getDefaultOption("service_name").value,
-        pod_name: "", // not supported yet
-        revision: "", // not supported yet
+        pod_name: "", // not supported
+        revision: GenericLogFilter.getDefaultOption("revision").value,
         output_stream: GenericLogFilter.getDefaultOption("output_stream").value,
     });
 
+    const revisionIdToNumber = useRevisionIdToNumber(appName, deploymentTargetId)
+    const latestRevisionNumber = useLatestRevisionNumber(appName, deploymentTargetId)
+
     const isAgentVersionUpdated = (agentImage: string | undefined) => {
         if (agentImage == null) {
             return false;
@@ -93,6 +101,15 @@ const Logs: React.FC<Props> = ({
         return patch >= 7;
     }
 
+    const createVersionOptions = (number: number) => {
+        return Array.from({ length: number }, (_, index) => {
+            const version = index + 1;
+            const label = version === number ? `Version ${version} (latest)` : `Version ${version}`;
+            const value = version.toString();
+            return GenericFilterOption.of(label, value);
+        }).reverse().slice(0, 3);
+    }
+
     const [filters, setFilters] = useState<GenericLogFilter[]>([
         {
             name: "service_name",
@@ -108,13 +125,26 @@ const Logs: React.FC<Props> = ({
                 }));
             }
         },
+        {
+            name: "revision",
+            displayName: "Version",
+            default: GenericLogFilter.getDefaultOption("revision"),
+            options: createVersionOptions(latestRevisionNumber),
+            setValue: (value: string) => {
+                setSelectedFilterValues((s) => ({
+                    ...s,
+                    revision: value,
+                }));
+            }
+        },
         {
             name: "output_stream",
             displayName: "Output Stream",
             default: GenericLogFilter.getDefaultOption("output_stream"),
-            options: serviceNames.map(s => {
-                return GenericFilterOption.of(s, s)
-            }) ?? [],
+            options: [
+                GenericFilterOption.of('stdout', 'stdout'),
+                GenericFilterOption.of("stderr", "stderr"),
+            ],
             setValue: (value: string) => {
                 setSelectedFilterValues((s) => ({
                     ...s,
@@ -142,9 +172,56 @@ const Logs: React.FC<Props> = ({
         enteredSearchText,
         notify,
         setIsLoading,
+        revisionIdToNumber,
         selectedDate,
     );
 
+    useEffect(() => {
+        setFilters([
+            {
+                name: "service_name",
+                displayName: "Service",
+                default: GenericLogFilter.getDefaultOption("service_name"),
+                options: serviceNames.map(s => {
+                    return GenericFilterOption.of(s, s)
+                }) ?? [],
+                setValue: (value: string) => {
+                    setSelectedFilterValues((s) => ({
+                        ...s,
+                        service_name: value,
+                    }));
+                }
+            },
+            {
+                name: "revision",
+                displayName: "Version",
+                default: GenericLogFilter.getDefaultOption("revision"),
+                options: createVersionOptions(latestRevisionNumber),
+                setValue: (value: string) => {
+                    setSelectedFilterValues((s) => ({
+                        ...s,
+                        revision: value,
+                    }));
+                }
+            },
+            {
+                name: "output_stream",
+                displayName: "Output Stream",
+                default: GenericLogFilter.getDefaultOption("output_stream"),
+                options: [
+                    GenericFilterOption.of('stdout', 'stdout'),
+                    GenericFilterOption.of("stderr", "stderr"),
+                ],
+                setValue: (value: string) => {
+                    setSelectedFilterValues((s) => ({
+                        ...s,
+                        output_stream: value,
+                    }));
+                }
+            },
+        ])
+    }, [latestRevisionNumber]);
+
     useEffect(() => {
         if (!isLoading && scrollToBottomRef.current && scrollToBottomEnabled) {
             const scrollPosition = scrollToBottomRef.current.offsetTop + scrollToBottomRef.current.offsetHeight - window.innerHeight;
@@ -159,8 +236,8 @@ const Logs: React.FC<Props> = ({
     const resetFilters = () => {
         setSelectedFilterValues({
             output_stream: GenericLogFilter.getDefaultOption("output_stream").value,
-            revision: "", // not supported yet
-            pod_name: "", // not supported yet
+            pod_name: "", // not supported
+            revision: GenericLogFilter.getDefaultOption("revision").value,
             service_name: GenericLogFilter.getDefaultOption("service_name").value,
         });
     };
@@ -246,6 +323,7 @@ const Logs: React.FC<Props> = ({
                                 <StyledLogs
                                     logs={logs}
                                     filters={filters}
+                                    appName={appName}
                                 />
                                 <LoadMoreButton
                                     active={selectedDate && logs.length !== 0}

+ 47 - 5
dashboard/src/main/home/app-dashboard/validate-apply/logs/utils.ts

@@ -4,8 +4,15 @@ import { useEffect, useRef, useState } from "react";
 import api from "shared/api";
 import Anser from "anser";
 import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets";
-import { AgentLog, agentLogValidator, Direction, PorterLog, PaginationInfo, LogFilterName } from "../../expanded-app/logs/types";
-import { Service } from "../../new-app-flow/serviceTypes";
+import {
+  AgentLog,
+  agentLogValidator,
+  Direction,
+  PorterLog,
+  PaginationInfo,
+  LogFilterName,
+  GenericLogFilter
+} from "../../expanded-app/logs/types";
 
 const MAX_LOGS = 5000;
 const MAX_BUFFER_LOGS = 1000;
@@ -44,7 +51,8 @@ export const useLogs = (
   searchParam: string,
   notify: (message: string) => void,
   setLoading: (isLoading: boolean) => void,
-  // if setDate is set, results are not live
+  revisionIdToNumber: Record<string, number>,
+    // if setDate is set, results are not live
   setDate?: Date,
   timeRange?: {
     startTime?: Dayjs,
@@ -125,7 +133,7 @@ export const useLogs = (
         }
       }
 
-      return updatedLogs;
+      return filterLogs(updatedLogs);
     });
   };
 
@@ -185,7 +193,8 @@ export const useLogs = (
           }
         });
         const newLogsParsed = parseLogs(newLogs);
-        pushLogs(newLogsParsed);
+        const newLogsFiltered = filterLogs(newLogsParsed);
+        pushLogs(newLogsFiltered);
       },
       onclose: () => {
         console.log("Closed websocket:", websocketKey);
@@ -196,6 +205,26 @@ export const useLogs = (
     openWebsocket(websocketKey);
   };
 
+  const filterLogs = (logs: PorterLog[]) => {
+    return logs.filter(log => {
+      if (log.metadata == null) {
+        return true;
+      }
+
+      if (selectedFilterValues.output_stream !== GenericLogFilter.getDefaultOption("output_stream").value &&
+          log.metadata.output_stream !== selectedFilterValues.output_stream) {
+        return false;
+      }
+
+      if (selectedFilterValues.revision !== GenericLogFilter.getDefaultOption("revision").value &&
+          log.metadata.revision !== selectedFilterValues.revision) {
+        return false;
+      }
+
+      return true;
+    });
+  };
+
   const queryLogs = async (
     startDate: string,
     endDate: string,
@@ -239,6 +268,19 @@ export const useLogs = (
       if (direction === Direction.backward) {
         newLogs.reverse();
       }
+
+      newLogs.filter((log) => {
+        return log.metadata?.raw_labels?.porter_run_app_revision_id != null
+            && revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id] != null
+            && revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id] != 0
+      }).forEach((log) => {
+        if (log.metadata?.raw_labels?.porter_run_app_revision_id != null) {
+            const revisionNumber = revisionIdToNumber[log.metadata.raw_labels.porter_run_app_revision_id];
+            if (revisionNumber != null && revisionNumber != 0) {
+              log.metadata.revision = revisionNumber.toString();
+            }
+      }})
+
       return {
         logs: newLogs,
         previousCursor:

+ 364 - 0
dashboard/src/main/home/app-dashboard/validate-apply/metrics/MetricsSection.tsx

@@ -0,0 +1,364 @@
+import React, {useEffect, useMemo, useState} from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+
+import TabSelector from "components/TabSelector";
+import SelectRow from "components/form-components/SelectRow";
+import { MetricNormalizer, resolutions, secondsBeforeNow } from "../../expanded-app/metrics/utils";
+import { Metric, MetricType, NginxStatusMetric } from "../../expanded-app/metrics/types";
+import { match } from "ts-pattern";
+import { AvailableMetrics, NormalizedMetricsData } from "main/home/cluster-dashboard/expanded-chart/metrics/types";
+import MetricsChart from "../../expanded-app/metrics/MetricsChart";
+import { useQuery } from "@tanstack/react-query";
+import Loading from "components/Loading";
+import CheckboxRow from "components/CheckboxRow";
+import {PorterApp} from "@porter-dev/api-contracts";
+
+type PropsType = {
+    projectId: number;
+    clusterId: number;
+    appName: string;
+    services: PorterApp["services"];
+    deploymentTargetId: string;
+};
+
+type ServiceOption = {
+    label: string;
+    value: string;
+}
+
+const MetricsSection: React.FunctionComponent<PropsType> = ({
+    projectId,
+    clusterId,
+    appName,
+    services,
+    deploymentTargetId,
+}) => {
+  const [selectedServiceName, setSelectedServiceName] = useState<string>("");
+  const [selectedRange, setSelectedRange] = useState("1H");
+  const [showAutoscalingThresholds, setShowAutoscalingThresholds] = useState(true);
+
+
+  const serviceOptions: ServiceOption[] = useMemo(() => {
+      return Object.keys(services).map((name) => {
+          return {
+              label: name,
+              value: name,
+          };
+      });
+  }, [services]);
+
+    useEffect(() => {
+        if (serviceOptions.length > 0) {
+            setSelectedServiceName(serviceOptions[0].value)
+        }
+    }, []);
+
+    const [serviceName, serviceKind, metricTypes, isHpaEnabled] = useMemo(() => {
+        if (selectedServiceName === "") {
+            return ["", "", [], false]
+        }
+
+        const service = services[selectedServiceName]
+
+        const serviceName = service.absoluteName === "" ? (appName + "-" + selectedServiceName) : service.absoluteName
+
+        let serviceKind = ""
+        const metricTypes: MetricType[] = ["cpu", "memory"];
+        let isHpaEnabled = false
+
+        if (service.config.case === "webConfig") {
+            serviceKind = "web"
+            metricTypes.push("network");
+            if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) {
+                isHpaEnabled = true
+            }
+            if (!service.config.value.private) {
+                metricTypes.push("nginx:status")
+            }
+        }
+
+        if (service.config.case === "workerConfig") {
+            serviceKind = "worker"
+            if (service.config.value.autoscaling != null && service.config.value.autoscaling.enabled) {
+                isHpaEnabled = true
+            }
+        }
+
+
+
+        if (isHpaEnabled) {
+            metricTypes.push("hpa_replicas");
+        }
+
+        return [serviceName, serviceKind, metricTypes, isHpaEnabled]
+    }, [selectedServiceName])
+
+
+  const { data: metricsData, isLoading: isMetricsDataLoading, refetch } = useQuery(
+    [
+        "getMetrics",
+        projectId,
+        clusterId,
+        serviceName,
+        selectedRange,
+        deploymentTargetId,
+    ],
+    async () => {
+
+      if (serviceName === "" || serviceKind === "" || metricTypes.length === 0) {
+        return;
+      }
+
+      const metrics: Metric[] = [];
+
+      const d = new Date();
+      const end = Math.round(d.getTime() / 1000);
+      const start = end - secondsBeforeNow[selectedRange];
+
+      for (const metricType of metricTypes) {
+          var kind = "";
+          if (serviceKind === "web") {
+              kind = "deployment";
+          } else if (serviceKind === "worker") {
+              kind = "deployment";
+          } else if (serviceKind === "job") {
+              kind = "job";
+          }
+          if (metricType === "nginx:status") {
+              kind = "Ingress"
+          }
+
+        const aggregatedMetricsResponse = await api.appMetrics(
+          "<token>",
+          {
+            metric: metricType,
+            shouldsum: false,
+            kind: kind,
+            name: serviceName,
+            deployment_target_id: deploymentTargetId,
+            startrange: start,
+            endrange: end,
+            resolution: resolutions[selectedRange],
+            pods: [],
+          },
+          {
+            id: projectId,
+            cluster_id: clusterId,
+          }
+        );
+
+        const metricsNormalizer = new MetricNormalizer(
+          [{ results: (aggregatedMetricsResponse.data ?? []).flatMap((d: any) => d.results) }],
+          metricType,
+        );
+        if (metricType === "nginx:status") {
+          const nginxMetric: NginxStatusMetric = {
+            type: metricType,
+            label: "Throughput",
+            areaData: metricsNormalizer.getNginxStatusData(),
+          }
+          metrics.push(nginxMetric)
+        } else {
+          const [data, allPodsAggregatedData] = metricsNormalizer.getAggregatedData();
+          const hpaData: NormalizedMetricsData[] = [];
+
+          if (isHpaEnabled && ["cpu", "memory"].includes(metricType)) {
+            let hpaMetricType = "cpu_hpa_threshold"
+            if (metricType === "memory") {
+              hpaMetricType = "memory_hpa_threshold"
+            }
+
+            const hpaRes = await api.appMetrics(
+              "<token>",
+              {
+                metric: hpaMetricType,
+                shouldsum: false,
+                kind: kind,
+                name: serviceName,
+                deployment_target_id: deploymentTargetId,
+                startrange: start,
+                endrange: end,
+                resolution: resolutions[selectedRange],
+                pods: [],
+              },
+              {
+                id: projectId,
+                cluster_id: clusterId,
+              }
+            );
+
+            const autoscalingMetrics = new MetricNormalizer(hpaRes.data, hpaMetricType as AvailableMetrics);
+            hpaData.push(...autoscalingMetrics.getParsedData());
+          }
+
+          const metric: Metric = match(metricType)
+            .with("cpu", () => ({
+              type: metricType,
+              label: "CPU Utilization (vCPUs)",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .with("memory", () => ({
+              type: metricType,
+              label: "RAM Utilization (Mi)",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .with("network", () => ({
+              type: metricType,
+              label: "Network Received Bytes (Ki)",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .with("hpa_replicas", () => ({
+              type: metricType,
+              label: "Number of replicas",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .with("nginx:errors", () => ({
+              type: metricType,
+              label: "5XX Error Percentage",
+              data: data,
+              aggregatedData: allPodsAggregatedData,
+              hpaData,
+            }))
+            .exhaustive();
+          metrics.push(metric);
+        }
+      };
+      return metrics;
+    },
+    {
+      enabled: selectedServiceName !== "",
+      refetchOnWindowFocus: false,
+      refetchInterval: 10000, // refresh metrics every 10 seconds
+    }
+  );
+
+  const renderMetrics = () => {
+    if (metricsData == null || isMetricsDataLoading) {
+      return <Loading />;
+    }
+    return metricsData.map((metric: Metric, i: number) => {
+      return (
+        <MetricsChart
+          key={metric.type}
+          metric={metric}
+          selectedRange={selectedRange}
+          isLoading={isMetricsDataLoading}
+          showAutoscalingLine={showAutoscalingThresholds}
+        />
+      );
+    })
+  }
+
+  const renderShowAutoscalingThresholdsCheckbox = (serviceName: string, isHpaEnabled: boolean) => {
+  if (serviceName === "") {
+    return null;
+  }
+
+    if (!isHpaEnabled) {
+      return null;
+    }
+    return (
+      <CheckboxRow
+        toggle={() => setShowAutoscalingThresholds(!showAutoscalingThresholds)}
+        checked={showAutoscalingThresholds}
+        label="Show Autoscaling Thresholds"
+      />
+    )
+  }
+
+  return (
+    <StyledMetricsSection>
+      <MetricsHeader>
+        <Flex>
+          <SelectRow
+            displayFlex={true}
+            label="Service"
+            value={selectedServiceName}
+            setActiveValue={(x: any) => setSelectedServiceName(x)}
+            options={serviceOptions}
+            width="200px"
+          />
+          <Highlight color={"#7d7d81"} onClick={() => refetch()}>
+            <i className="material-icons">autorenew</i>
+          </Highlight>
+          {renderShowAutoscalingThresholdsCheckbox(serviceName, isHpaEnabled)}
+        </Flex>
+        <RangeWrapper>
+          <Relative>
+          </Relative>
+          <TabSelector
+            noBuffer={true}
+            options={[
+              { value: "1H", label: "1H" },
+              { value: "6H", label: "6H" },
+              { value: "1D", label: "1D" },
+              { value: "1M", label: "1M" },
+            ]}
+            currentTab={selectedRange}
+            setCurrentTab={(x: string) => setSelectedRange(x)}
+          />
+        </RangeWrapper>
+      </MetricsHeader>
+      {renderMetrics()}
+    </StyledMetricsSection>
+  );
+};
+
+export default MetricsSection;
+
+const Relative = styled.div`
+  position: relative;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const MetricsHeader = styled.div`
+  width: 100%;
+  display: flex;
+  align-items: center;
+  overflow: visible;
+  justify-content: space-between;
+`;
+
+const RangeWrapper = styled.div`
+  float: right;
+  font-weight: bold;
+  width: 158px;
+  margin-top: -8px;
+`;
+
+const StyledMetricsSection = styled.div`
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+`;
+
+const Highlight = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-left: 8px;
+  margin-bottom: 15px;
+  margin-top: 20px;
+  color: ${(props: { color: string }) => props.color};
+  cursor: pointer;
+
+  > i {
+    font-size: 20px;
+    margin-right: 3px;
+  }
+`;

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

@@ -26,7 +26,7 @@ interface ServiceProps {
   index: number;
   service: ClientService;
   chart?: any;
-  update: UseFieldArrayUpdate<PorterAppFormData, "app.services">;
+  update: UseFieldArrayUpdate<PorterAppFormData, "app.services" | "app.predeploy">;
   remove: (index: number) => void;
 }
 
@@ -47,7 +47,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   const [maxRAM, setMaxRAM] = useState(
     Math.round(
       convert(AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"], "GiB").to("MB") *
-        UPPER_BOUND
+      UPPER_BOUND
     )
   ); //default is set to a t3 medium
   const context = useContext(Context);
@@ -140,7 +140,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
             );
           }
         })
-        .catch((error) => {});
+        .catch((error) => { });
     }
   }, []);
 
@@ -246,7 +246,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
         // Check if has built image
         getHasBuiltImage() && (
           <StatusFooter
-            setExpandedJob={() => {}}
+            setExpandedJob={() => { }}
             chart={chart}
             service={service}
           />
@@ -331,7 +331,7 @@ const ServiceHeader = styled.div<{
     border-radius: 20px;
     margin-left: -10px;
     transform: ${(props: { showExpanded?: boolean; chart: any }) =>
-      props.showExpanded ? "" : "rotate(-90deg)"};
+    props.showExpanded ? "" : "rotate(-90deg)"};
   }
 `;
 

+ 9 - 2
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx

@@ -43,12 +43,16 @@ type ServiceListProps = {
   addNewText: string;
   prePopulateService?: ClientService;
   isPredeploy?: boolean;
+  existingServiceNames?: string[];
+  fieldArrayName: "app.services" | "app.predeploy";
 };
 
 const ServiceList: React.FC<ServiceListProps> = ({
   addNewText,
   prePopulateService,
   isPredeploy = false,
+  existingServiceNames = [],
+  fieldArrayName,
 }) => {
   // top level app form
   const { control: appControl } = useFormContext<PorterAppFormData>();
@@ -70,7 +74,7 @@ const ServiceList: React.FC<ServiceListProps> = ({
   });
   const { append, remove, update, fields } = useFieldArray({
     control: appControl,
-    name: "app.services",
+    name: fieldArrayName,
   });
   const {
     append: appendDeletion,
@@ -150,7 +154,10 @@ const ServiceList: React.FC<ServiceListProps> = ({
   const onRemove = (index: number) => {
     const name = services[index].svc.name.value;
     remove(index);
-    appendDeletion({ name });
+
+    if (existingServiceNames.includes(name)) {
+      appendDeletion({ name });
+    }
   };
 
   return (

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

@@ -1,19 +1,16 @@
 import React from "react";
 import Button from "components/porter/Button";
 import styled from "styled-components";
-import Input from "components/porter/Input";
 import Spacer from "components/porter/Spacer";
 import { useFieldArray, useFormContext } from "react-hook-form";
 import { PorterAppFormData } from "lib/porter-apps";
-import { ClientDomains } from "lib/porter-apps/values";
 import { ControlledInput } from "components/porter/ControlledInput";
 
 interface Props {
   index: number;
-  customDomains: ClientDomains;
 }
 
-const CustomDomains: React.FC<Props> = ({ index, customDomains }) => {
+const CustomDomains: React.FC<Props> = ({ index }) => {
   const { control, register } = useFormContext<PorterAppFormData>();
   const { remove, append, fields } = useFieldArray({
     control,

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

@@ -56,7 +56,7 @@ const Health: React.FC<HealthProps> = ({ index, service }) => {
               "You may only edit this field in your porter.yaml."
             }
           >
-            <Text color="helper">Enable Liveness Probe</Text>
+            <Text color="helper">Enable health checks</Text>
           </Checkbox>
         )}
       />

+ 9 - 8
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx

@@ -39,14 +39,14 @@ const JobTabs: React.FC<Props> = ({
 
   const tabs = isPredeploy
     ? [
-        { label: "Main", value: "main" as const },
-        { label: "Resources", value: "resources" as const },
-      ]
+      { label: "Main", value: "main" as const },
+      { label: "Resources", value: "resources" as const },
+    ]
     : [
-        { label: "Main", value: "main" as const },
-        { label: "Resources", value: "resources" as const },
-        { label: "Advanced", value: "advanced" as const },
-      ];
+      { label: "Main", value: "main" as const },
+      { label: "Resources", value: "resources" as const },
+      { label: "Advanced", value: "advanced" as const },
+    ];
 
   return (
     <>
@@ -56,13 +56,14 @@ const JobTabs: React.FC<Props> = ({
         setCurrentTab={setCurrentTab}
       />
       {match(currentTab)
-        .with("main", () => <MainTab index={index} service={service} />)
+        .with("main", () => <MainTab index={index} service={service} isPredeploy={isPredeploy} />)
         .with("resources", () => (
           <Resources
             index={index}
             maxCPU={maxCPU}
             maxRAM={maxRAM}
             service={service}
+            isPredeploy={isPredeploy}
           />
         ))
         .with("advanced", () => (

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

@@ -12,9 +12,10 @@ import Link from "components/porter/Link";
 type MainTabProps = {
   index: number;
   service: ClientService;
+  isPredeploy?: boolean;
 };
 
-const MainTab: React.FC<MainTabProps> = ({ index, service }) => {
+const MainTab: React.FC<MainTabProps> = ({ index, service, isPredeploy = false }) => {
   const { register, watch } = useFormContext<PorterAppFormData>();
   const cron = watch(`app.services.${index}.config.cron.value`);
 
@@ -45,7 +46,7 @@ const MainTab: React.FC<MainTabProps> = ({ index, service }) => {
         width="300px"
         disabled={service.run.readOnly}
         disabledTooltip={"You may only edit this field in your porter.yaml."}
-        {...register(`app.services.${index}.run.value`)}
+        {...register(isPredeploy ? `app.predeploy.${index}.run.value` : `app.services.${index}.run.value`)}
       />
       {service.config.type === "job" && (
         <>

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

@@ -6,7 +6,6 @@ import { Controller, useFormContext } from "react-hook-form";
 import { PorterAppFormData } from "lib/porter-apps";
 import Checkbox from "components/porter/Checkbox";
 import Text from "components/porter/Text";
-import AnimateHeight from "react-animate-height";
 import CustomDomains from "./CustomDomains";
 
 type NetworkingProps = {
@@ -66,7 +65,7 @@ const Networking: React.FC<NetworkingProps> = ({ index, service }) => {
         control={control}
         render={({ field: { value, onChange } }) => (
           <Checkbox
-            checked={value}
+            checked={!value}
             disabled={service.config.private.readOnly}
             toggleChecked={() => {
               onChange(!value);
@@ -75,7 +74,7 @@ const Networking: React.FC<NetworkingProps> = ({ index, service }) => {
               "You may only edit this field in your porter.yaml."
             }
           >
-            <Text color="helper">Private Service</Text>
+            <Text color="helper">Expose to external traffic</Text>
           </Checkbox>
         )}
       />
@@ -94,7 +93,7 @@ const Networking: React.FC<NetworkingProps> = ({ index, service }) => {
             </a>
           </Text>
           <Spacer y={0.5} />
-          <CustomDomains index={index} customDomains={service.config.domains} />
+          <CustomDomains index={index} />
           <Spacer y={0.5} />
         </>
       )}

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

@@ -14,6 +14,7 @@ type ResourcesProps = {
   maxCPU: number;
   maxRAM: number;
   service: ClientService;
+  isPredeploy?: boolean;
 };
 
 const Resources: React.FC<ResourcesProps> = ({
@@ -21,6 +22,7 @@ const Resources: React.FC<ResourcesProps> = ({
   maxCPU,
   maxRAM,
   service,
+  isPredeploy = false,
 }) => {
   const { control, register, watch } = useFormContext<PorterAppFormData>();
 
@@ -32,7 +34,7 @@ const Resources: React.FC<ResourcesProps> = ({
     <>
       <Spacer y={1} />
       <Controller
-        name={`app.services.${index}.cpuCores`}
+        name={isPredeploy ? `app.predeploy.${index}.cpuCores` : `app.services.${index}.cpuCores`}
         control={control}
         render={({ field: { value, onChange } }) => (
           <InputSlider
@@ -58,7 +60,7 @@ const Resources: React.FC<ResourcesProps> = ({
       />
       <Spacer y={1} />
       <Controller
-        name={`app.services.${index}.ramMegabytes`}
+        name={isPredeploy ? `app.predeploy.${index}.ramMegabytes` : `app.services.${index}.ramMegabytes`}
         control={control}
         render={({ field: { value, onChange } }) => (
           <InputSlider

+ 1 - 1
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -248,7 +248,7 @@ class SettingsPage extends Component<PropsType, StateType> {
         <StyledSettingsPage>
           {this.renderHeaderSection()}
           {this.props.isCloning && this.getNameInput()}
-          {!currentProject?.capi_provisioner_enabled && (
+          {!currentProject?.simplified_view_enabled && (
             <>
               <Heading>Destination</Heading>
               <Helper>

+ 6 - 6
dashboard/src/main/home/sidebar/ProjectButton.tsx

@@ -3,7 +3,7 @@ import styled from "styled-components";
 import gradient from "assets/gradient.png";
 
 import { Context } from "shared/Context";
-import { ProjectType } from "shared/types";
+import { ProjectListType, ProjectType } from "shared/types";
 import { pushFiltered } from "shared/routing";
 import { RouteComponentProps, withRouter } from "react-router";
 import Icon from "components/porter/Icon";
@@ -13,14 +13,14 @@ import ProjectSelectionModal from "./ProjectSelectionModal";
 
 type PropsType = RouteComponentProps & {
   currentProject: ProjectType;
-  projects: ProjectType[];
+  projects: ProjectListType[];
 };
 
 const ProjectButton: React.FC<PropsType> = (props) => {
   const [expanded, setExpanded] = useState(false);
   const wrapperRef = useRef<any>(null);
   const context = useContext(Context);
-  const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
+  const [showModal, setShowModal] = useState<boolean>(false);
 
   const { user } = context;
 
@@ -51,17 +51,17 @@ const ProjectButton: React.FC<PropsType> = (props) => {
   if (currentProject) {
     return (
       <StyledProjectSection ref={wrapperRef}>
-        {showGHAModal && currentProject != null && (
+        {showModal && currentProject != null && (
           <ProjectSelectionModal
             currentProject={props.currentProject}
             projects={props.projects}
-            closeModal={() => setShowGHAModal(false)}
+            closeModal={() => setShowModal(false)}
           />
         )}
         <MainSelector
           projectsLength={props.projects.length}
           isPorterUser={user.isPorterUser}
-          onClick={() => (props.projects.length > 1 || user.isPorterUser) && setShowGHAModal(true)} >
+          onClick={() => (props.projects.length > 1 || user.isPorterUser) && setShowModal(true)} >
           <ProjectIcon>
             <ProjectImage src={gradient} />
             <Letter>{currentProject.name[0].toUpperCase()}</Letter>

+ 0 - 27
dashboard/src/main/home/sidebar/ProjectButtonContainer.tsx

@@ -1,27 +0,0 @@
-import React, { Component } from "react";
-
-import { Context } from "shared/Context";
-import ProjectButton from "./ProjectButton";
-
-type PropsType = {};
-
-type StateType = {};
-
-// Props in context to project section to trigger update on context change
-export default class ProjectButtonContianer extends Component<
-  PropsType,
-  StateType
-> {
-  state = {};
-
-  render() {
-    return (
-      <ProjectButton
-        currentProject={this.context.currentProject}
-        projects={this.context.projects}
-      />
-    );
-  }
-}
-
-ProjectButtonContianer.contextType = Context;

+ 6 - 10
dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx

@@ -6,7 +6,7 @@ import Modal from "components/porter/Modal";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import { Context } from "shared/Context";
-import { DetailedClusterType, ProjectType } from "shared/types";
+import { DetailedClusterType, ProjectListType, ProjectType } from "shared/types";
 import { pushFiltered } from "shared/routing";
 import SearchBar from "components/porter/SearchBar";
 import _ from 'lodash';
@@ -17,7 +17,7 @@ import Container from "components/porter/Container";
 
 type Props = RouteComponentProps & {
   closeModal: () => void;
-  projects: ProjectType[];
+  projects: ProjectListType[];
   currentProject: ProjectType;
 }
 
@@ -103,19 +103,15 @@ const ProjectSelectionModal: React.FC<Props> = ({
             const clusters_list = await updateClusterList(project.id);
             if (clusters_list?.length > 0) {
               setCurrentCluster(clusters_list[0]);
-              if (project.simplified_view_enabled) {
-                pushFiltered(props, "/apps", ["project_id"], {});
-              }
-              else {
-                pushFiltered(props, "/applications", ["project_id"], {});
-              }
+              pushFiltered(props, "/dashboard", ["project_id"]);
             } else {
-              pushFiltered(props, "/apps", ["project_id"], {});
+              setCurrentProject(project, () => {
+                pushFiltered(props, "/dashboard", ["project_id"]);
+              });
             }
             closeModal();
           }}
         >
-          {/* <BlockIcon src={gradient} /> */}
           <BlockTitle>{projectListEntry.name}</BlockTitle>
 
 

+ 0 - 2
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -23,8 +23,6 @@ import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import SidebarLink from "./SidebarLink";
 import { overrideInfraTabEnabled } from "utils/infrastructure";
 import ClusterListContainer from "./ClusterListContainer";
-import ProjectButtonContainer from "./ProjectButtonContainer";
-import ProjectButtonContianer from "./ProjectButtonContainer";
 
 type PropsType = RouteComponentProps &
   WithAuthProps & {

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

@@ -1476,6 +1476,27 @@ const getMetrics = baseApi<
   return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/metrics`;
 });
 
+const appMetrics = baseApi<
+    {
+        metric: string;
+        shouldsum: boolean;
+        pods?: string[];
+        kind?: string; // the controller kind
+        name?: string;
+        percentile?: number;
+        deployment_target_id: string;
+        startrange: number;
+        endrange: number;
+        resolution: string;
+    },
+    {
+        id: number;
+        cluster_id: number;
+    }
+>("GET", (pathParams) => {
+    return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/apps/metrics`;
+});
+
 const getNamespaces = baseApi<
   {},
   {
@@ -3007,6 +3028,7 @@ export default {
   getAllReleasePods,
   getClusterState,
   getMetrics,
+  appMetrics,
   getNamespaces,
   getNGINXIngresses,
   getOAuthIds,

+ 11 - 1
go.mod

@@ -76,10 +76,12 @@ require (
 	github.com/glebarez/sqlite v1.6.0
 	github.com/go-chi/chi/v5 v5.0.8
 	github.com/honeycombio/otel-config-go v1.11.0
+	github.com/launchdarkly/go-sdk-common/v3 v3.0.1
+	github.com/launchdarkly/go-server-sdk/v6 v6.1.0
 	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.0.98
+	github.com/porter-dev/api-contracts v0.0.99
 	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
@@ -136,11 +138,18 @@ require (
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
+	github.com/launchdarkly/ccache v1.1.0 // indirect
+	github.com/launchdarkly/eventsource v1.6.2 // indirect
+	github.com/launchdarkly/go-jsonstream/v3 v3.0.0 // indirect
+	github.com/launchdarkly/go-sdk-events/v2 v2.0.1 // indirect
+	github.com/launchdarkly/go-semver v1.0.2 // indirect
+	github.com/launchdarkly/go-server-sdk-evaluation/v2 v2.0.2 // indirect
 	github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/nats-io/nats-server/v2 v2.9.15 // indirect
 	github.com/nats-io/nkeys v0.3.0 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
+	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
 	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
@@ -169,6 +178,7 @@ require (
 	go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
 	istio.io/api v0.0.0-20221109202042-b9e5d446a83d // indirect

+ 30 - 2
go.sum

@@ -1123,6 +1123,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0=
 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
+github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003 h1:vJ0Snvo+SLMY72r5J4sEfkuE7AFbixEP2qRbEcum/wA=
+github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003/go.mod h1:zNBxMY8P21owkeogJELCLeHIt+voOSduHYTFUbwRAV8=
 github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
 github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
 github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
@@ -1183,6 +1185,25 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
 github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
 github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
+github.com/launchdarkly/ccache v1.1.0 h1:voD1M+ZJXR3MREOKtBwgTF9hYHl1jg+vFKS/+VAkR2k=
+github.com/launchdarkly/ccache v1.1.0/go.mod h1:TlxzrlnzvYeXiLHmesMuvoZetu4Z97cV1SsdqqBJi1Q=
+github.com/launchdarkly/eventsource v1.6.2 h1:5SbcIqzUomn+/zmJDrkb4LYw7ryoKFzH/0TbR0/3Bdg=
+github.com/launchdarkly/eventsource v1.6.2/go.mod h1:LHxSeb4OnqznNZxCSXbFghxS/CjIQfzHovNoAqbO/Wk=
+github.com/launchdarkly/go-jsonstream/v3 v3.0.0 h1:qJF/WI09EUJ7kSpmP5d1Rhc81NQdYUhP17McKfUq17E=
+github.com/launchdarkly/go-jsonstream/v3 v3.0.0/go.mod h1:/1Gyml6fnD309JOvunOSfyysWbZ/ZzcA120gF/cQtC4=
+github.com/launchdarkly/go-sdk-common/v3 v3.0.1 h1:rVdLusAIViduNvyjNKy06RA+SPwk0Eq+NocNd1opDhk=
+github.com/launchdarkly/go-sdk-common/v3 v3.0.1/go.mod h1:H/zISoCNhviHTTqqBjIKQy2YgSHT8ioL1FtgBKpiEGg=
+github.com/launchdarkly/go-sdk-events/v2 v2.0.1 h1:vnUN2Y7og/5wtOCcCZW7wYpmZcS++GAyclasc7gaTIY=
+github.com/launchdarkly/go-sdk-events/v2 v2.0.1/go.mod h1:Msqbl6brgFO83RUxmLaJAUx2sYG+WKULcy+Vf3+tKww=
+github.com/launchdarkly/go-semver v1.0.2 h1:sYVRnuKyvxlmQCnCUyDkAhtmzSFRoX6rG2Xa21Mhg+w=
+github.com/launchdarkly/go-semver v1.0.2/go.mod h1:xFmMwXba5Mb+3h72Z+VeSs9ahCvKo2QFUTHRNHVqR28=
+github.com/launchdarkly/go-server-sdk-evaluation/v2 v2.0.2 h1:PAM0GvE0nIUBeOkjdiymIEKI+8FFLJ+fEsWTupW1yGU=
+github.com/launchdarkly/go-server-sdk-evaluation/v2 v2.0.2/go.mod h1:Mztipcz+7ZMatXVun3k/IfPa8IOgUnAqiZawtFh2MRg=
+github.com/launchdarkly/go-server-sdk/v6 v6.1.0 h1:pp/VBIon5JNbtSw7ih7I5MXN9mXHfo6T5xikKVy52dI=
+github.com/launchdarkly/go-server-sdk/v6 v6.1.0/go.mod h1:3TfS6vNsNksPT4LGeGuGKtT3zTjTbbOsCUK3nrj1neA=
+github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs=
+github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw=
+github.com/launchdarkly/go-test-helpers/v3 v3.0.2 h1:rh0085g1rVJM5qIukdaQ8z1XTWZztbJ49vRZuveqiuU=
 github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0=
 github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88=
 github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
@@ -1462,6 +1483,8 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6
 github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
 github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -1489,8 +1512,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.0.98 h1:pchO+C7HKhpWZzR2RPDKKJnH3Rx8Cy/Q1v9aTfR9jzk=
-github.com/porter-dev/api-contracts v0.0.98/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.0.99 h1:7VltsUOtlTPlTApmcFyAhC29QxptgS87JNoeUk7VWGk=
+github.com/porter-dev/api-contracts v0.0.99/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=
@@ -1708,6 +1731,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -1780,6 +1804,8 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17
 github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/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/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
+github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ=
+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=
@@ -1973,6 +1999,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
+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-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

+ 768 - 0
go.work.sum

@@ -1,270 +1,979 @@
+4d63.com/gochecknoglobals v0.1.0 h1:zeZSRqj5yCg28tCkIV/z/lWbwvNm5qnKVS15PI8nhD0=
+bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898 h1:SC+c6A1qTFstO9qmB86mPV2IpYme/2ZoEQ0hrP+wo+Q=
+bitbucket.org/creachadair/shell v0.0.6 h1:reJflDbKqnlnqb4Oo2pQ1/BqmY/eCWcNGHrIUO8qIzc=
 cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
+cloud.google.com/go/accessapproval v1.6.0 h1:x0cEHro/JFPd7eS4BlEWNTMecIj2HdXjOVB5BtvwER0=
 cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
 cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE=
+cloud.google.com/go/accesscontextmanager v1.7.0 h1:MG60JgnEoawHJrbWw0jGdv6HLNSf6gQvYRiXpuzqgEA=
 cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ=
 cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg=
+cloud.google.com/go/aiplatform v1.37.0 h1:zTw+suCVchgZyO+k847wjzdVjWmrAuehxdvcZvJwfGg=
 cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw=
 cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=
+cloud.google.com/go/analytics v0.19.0 h1:LqAo3tAh2FU9+w/r7vc3hBjU23Kv7GhO/PDIW7kIYgM=
 cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE=
 cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc=
+cloud.google.com/go/apigateway v1.5.0 h1:ZI9mVO7x3E9RK/BURm2p1aw9YTBSCQe3klmyP1WxWEg=
 cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8=
 cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04=
+cloud.google.com/go/apigeeconnect v1.5.0 h1:sWOmgDyAsi1AZ48XRHcATC0tsi9SkPT7DA/+VCfkaeA=
 cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8=
+cloud.google.com/go/apigeeregistry v0.6.0 h1:E43RdhhCxdlV+I161gUY2rI4eOaMzHTA5kNkvRsFXvc=
 cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc=
+cloud.google.com/go/apikeys v0.6.0 h1:B9CdHFZTFjVti89tmyXXrO+7vSNo2jvZuHG8zD5trdQ=
+cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8=
 cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak=
+cloud.google.com/go/appengine v1.7.1 h1:aBGDKmRIaRRoWJ2tAoN0oVSHoWLhtO9aj/NvUyP4aYs=
 cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E=
 cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=
+cloud.google.com/go/area120 v0.7.1 h1:ugckkFh4XkHJMPhTIx0CyvdoBxmOpMe8rNs4Ok8GAag=
 cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k=
 cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY=
+cloud.google.com/go/asset v1.13.0 h1:YAsssO08BqZ6mncbb6FPlj9h6ACS7bJQUOlzciSfbNk=
 cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw=
 cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
+cloud.google.com/go/assuredworkloads v1.10.0 h1:VLGnVFta+N4WM+ASHbhc14ZOItOabDLH1MSoDv+Xuag=
 cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
 cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM=
+cloud.google.com/go/automl v1.12.0 h1:50VugllC+U4IGl3tDNcZaWvApHBTrn/TvyHDJ0wM+Uw=
 cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU=
 cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI=
+cloud.google.com/go/baremetalsolution v0.5.0 h1:2AipdYXL0VxMboelTTw8c1UJ7gYu35LZYUbuRv9Q28s=
 cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss=
 cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE=
+cloud.google.com/go/batch v0.7.0 h1:YbMt0E6BtqeD5FvSv1d56jbVsWEzlGm55lYte+M6Mzs=
 cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g=
 cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8=
+cloud.google.com/go/beyondcorp v0.5.0 h1:UkY2BTZkEUAVrgqnSdOJ4p3y9ZRBPEe1LkjgC8Bj/Pc=
 cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU=
 cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc=
+cloud.google.com/go/bigquery v1.50.0 h1:RscMV6LbnAmhAzD893Lv9nXXy2WCaJmbxYPWDLbGqNQ=
 cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU=
 cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y=
+cloud.google.com/go/billing v1.13.0 h1:JYj28UYF5w6VBAh0gQYlgHJ/OD1oA+JgW29YZQU+UHM=
 cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc=
 cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk=
+cloud.google.com/go/binaryauthorization v1.5.0 h1:d3pMDBCCNivxt5a4eaV7FwL7cSH0H7RrEnFrTb1QKWs=
 cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q=
 cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590=
+cloud.google.com/go/certificatemanager v1.6.0 h1:5C5UWeSt8Jkgp7OWn2rCkLmYurar/vIWIoSQ2+LaTOc=
 cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8=
 cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk=
+cloud.google.com/go/channel v1.12.0 h1:GpcQY5UJKeOekYgsX3QXbzzAc/kRGtBq43fTmyKe6Uw=
 cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU=
 cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA=
+cloud.google.com/go/cloudbuild v1.9.0 h1:GHQCjV4WlPPVU/j3Rlpc8vNIDwThhd1U9qSY/NPZdko=
 cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s=
 cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk=
+cloud.google.com/go/clouddms v1.5.0 h1:E7v4TpDGUyEm1C/4KIrpVSOCTm0P6vWdHT0I4mostRA=
 cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA=
 cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI=
+cloud.google.com/go/cloudtasks v1.10.0 h1:uK5k6abf4yligFgYFnG0ni8msai/dSv6mDmiBulU0hU=
 cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs=
 cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
 cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
 cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
 cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
+cloud.google.com/go/contactcenterinsights v1.6.0 h1:jXIpfcH/VYSE1SYcPzO0n1VVb+sAamiLOgCw45JbOQk=
 cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
 cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo=
+cloud.google.com/go/container v1.15.0 h1:NKlY/wCDapfVZlbVVaeuu2UZZED5Dy1z4Zx1KhEzm8c=
 cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA=
 cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=
+cloud.google.com/go/containeranalysis v0.9.0 h1:EQ4FFxNaEAg8PqQCO7bVQfWz9NVwZCUKaM1b3ycfx3U=
 cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s=
 cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM=
+cloud.google.com/go/datacatalog v1.13.0 h1:4H5IJiyUE0X6ShQBqgFFZvGGcrwGVndTwUSLP4c52gw=
 cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8=
 cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=
+cloud.google.com/go/dataflow v0.8.0 h1:eYyD9o/8Nm6EttsKZaEGD84xC17bNgSKCu0ZxwqUbpg=
 cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE=
 cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0=
+cloud.google.com/go/dataform v0.7.0 h1:Dyk+fufup1FR6cbHjFpMuP4SfPiF3LI3JtoIIALoq48=
 cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE=
 cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w=
+cloud.google.com/go/datafusion v1.6.0 h1:sZjRnS3TWkGsu1LjYPFD/fHeMLZNXDK6PDHi2s2s/bk=
 cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8=
 cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=
+cloud.google.com/go/datalabeling v0.7.0 h1:ch4qA2yvddGRUrlfwrNJCr79qLqhS9QBwofPHfFlDIk=
 cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM=
 cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A=
+cloud.google.com/go/dataplex v1.6.0 h1:RvoZ5T7gySwm1CHzAw7yY1QwwqaGswunmqEssPxU/AM=
 cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs=
 cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI=
+cloud.google.com/go/dataproc v1.12.0 h1:W47qHL3W4BPkAIbk4SWmIERwsWBaNnWm0P2sdx3YgGU=
 cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4=
 cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=
+cloud.google.com/go/dataqna v0.7.0 h1:yFzi/YU4YAdjyo7pXkBE2FeHbgz5OQQBVDdbErEHmVQ=
 cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c=
 cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM=
+cloud.google.com/go/datastore v1.11.0 h1:iF6I/HaLs3Ado8uRKMvZRvF/ZLkWaWE9i8AiHzbC774=
 cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c=
 cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4=
+cloud.google.com/go/datastream v1.7.0 h1:BBCBTnWMDwwEzQQmipUXxATa7Cm7CA/gKjKcR2w35T0=
 cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww=
 cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s=
+cloud.google.com/go/deploy v1.8.0 h1:otshdKEbmsi1ELYeCKNYppwV0UH5xD05drSdBm7ouTk=
 cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ=
 cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0=
+cloud.google.com/go/dialogflow v1.32.0 h1:uVlKKzp6G/VtSW0E7IH1Y5o0H48/UOCmqksG2riYCwQ=
 cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE=
 cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q=
+cloud.google.com/go/dlp v1.9.0 h1:1JoJqezlgu6NWCroBxr4rOZnwNFILXr4cB9dMaSKO4A=
 cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4=
 cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4=
+cloud.google.com/go/documentai v1.18.0 h1:KM3Xh0QQyyEdC8Gs2vhZfU+rt6OCPF0dwVwxKgLmWfI=
 cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs=
 cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=
+cloud.google.com/go/domains v0.8.0 h1:2ti/o9tlWL4N+wIuWUNH+LbfgpwxPr8J1sv9RHA4bYQ=
 cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE=
 cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=
+cloud.google.com/go/edgecontainer v1.0.0 h1:O0YVE5v+O0Q/ODXYsQHmHb+sYM8KNjGZw2pjX2Ws41c=
 cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY=
+cloud.google.com/go/errorreporting v0.3.0 h1:kj1XEWMu8P0qlLhm3FwcaFsUvXChV/OraZwA70trRR0=
 cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU=
 cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8=
+cloud.google.com/go/essentialcontacts v1.5.0 h1:gIzEhCoOT7bi+6QZqZIzX1Erj4SswMPIteNvYVlu+pM=
 cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M=
 cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw=
+cloud.google.com/go/eventarc v1.11.0 h1:fsJmNeqvqtk74FsaVDU6cH79lyZNCYP8Rrv7EhaB/PU=
 cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY=
 cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI=
+cloud.google.com/go/filestore v1.6.0 h1:ckTEXN5towyTMu4q0uQ1Mde/JwTHur0gXs8oaIZnKfw=
 cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg=
+cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
 cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
 cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08=
+cloud.google.com/go/functions v1.13.0 h1:pPDqtsXG2g9HeOQLoquLbmvmb82Y4Ezdo1GXuotFoWg=
 cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c=
 cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM=
+cloud.google.com/go/gaming v1.9.0 h1:7vEhFnZmd931Mo7sZ6pJy7uQPDxF7m7v8xtBheG08tc=
 cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0=
 cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo=
+cloud.google.com/go/gkebackup v0.4.0 h1:za3QZvw6ujR0uyqkhomKKKNoXDyqYGPJies3voUK8DA=
 cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg=
 cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=
+cloud.google.com/go/gkeconnect v0.7.0 h1:gXYKciHS/Lgq0GJ5Kc9SzPA35NGc3yqu6SkjonpEr2Q=
 cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw=
 cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=
+cloud.google.com/go/gkehub v0.12.0 h1:TqCSPsEBQ6oZSJgEYZ3XT8x2gUadbvfwI32YB0kuHCs=
 cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw=
 cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI=
+cloud.google.com/go/gkemulticloud v0.5.0 h1:8I84Q4vl02rJRsFiinBxl7WCozfdLlUVBQuSrqr9Wtk=
 cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y=
+cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM=
+cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
 cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o=
+cloud.google.com/go/gsuiteaddons v1.5.0 h1:1mvhXqJzV0Vg5Fa95QwckljODJJfDFXV4pn+iL50zzA=
 cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo=
 cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=
+cloud.google.com/go/iap v1.7.1 h1:PxVHFuMxmSZyfntKXHXhd8bo82WJ+LcATenq7HLdVnU=
 cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74=
 cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY=
+cloud.google.com/go/ids v1.3.0 h1:fodnCDtOXuMmS8LTC2y3h8t24U8F3eKWfhi+3LY6Qf0=
 cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4=
 cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g=
+cloud.google.com/go/iot v1.6.0 h1:39W5BFSarRNZfVG0eXI5LYux+OVQT8GkgpHCnrZL2vM=
 cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE=
 cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0=
+cloud.google.com/go/kms v1.10.1 h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g=
 cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI=
 cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8=
+cloud.google.com/go/language v1.9.0 h1:7Ulo2mDk9huBoBi8zCE3ONOoBrL6UXfAI71CLQ9GEIM=
 cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY=
 cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=
+cloud.google.com/go/lifesciences v0.8.0 h1:uWrMjWTsGjLZpCTWEAzYvyXj+7fhiZST45u9AgasasI=
 cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo=
 cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw=
+cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
 cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
 cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM=
+cloud.google.com/go/managedidentities v1.5.0 h1:ZRQ4k21/jAhrHBVKl/AY7SjgzeJwG1iZa+mJ82P+VNg=
 cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA=
+cloud.google.com/go/maps v0.7.0 h1:mv9YaczD4oZBZkM5XJl6fXQ984IkJNHPwkc8MUsdkBo=
 cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY=
 cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=
+cloud.google.com/go/mediatranslation v0.7.0 h1:anPxH+/WWt8Yc3EdoEJhPMBRF7EhIdz426A+tuoA0OU=
 cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I=
 cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY=
+cloud.google.com/go/memcache v1.9.0 h1:8/VEmWCpnETCrBwS3z4MhT+tIdKgR1Z4Tr2tvYH32rg=
 cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM=
 cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI=
+cloud.google.com/go/metastore v1.10.0 h1:QCFhZVe2289KDBQ7WxaHV2rAmPrmRAdLC6gbjUd3HPo=
 cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo=
 cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4=
+cloud.google.com/go/monitoring v1.13.0 h1:2qsrgXGVoRXpP7otZ14eE1I568zAa92sJSDPyOJvwjM=
 cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw=
 cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8=
+cloud.google.com/go/networkconnectivity v1.11.0 h1:ZD6b4Pk1jEtp/cx9nx0ZYcL3BKqDa+KixNDZ6Bjs1B8=
 cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM=
 cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4=
+cloud.google.com/go/networkmanagement v1.6.0 h1:8KWEUNGcpSX9WwZXq7FtciuNGPdPdPN/ruDm769yAEM=
 cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY=
 cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=
+cloud.google.com/go/networksecurity v0.8.0 h1:sOc42Ig1K2LiKlzG71GUVloeSJ0J3mffEBYmvu+P0eo=
 cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU=
 cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0=
+cloud.google.com/go/notebooks v1.8.0 h1:Kg2K3K7CbSXYJHZ1aGQpf1xi5x2GUvQWf2sFVuiZh8M=
 cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ=
 cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs=
+cloud.google.com/go/optimization v1.3.1 h1:dj8O4VOJRB4CUwZXdmwNViH1OtI0WtWL867/lnYH248=
 cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI=
 cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk=
+cloud.google.com/go/orchestration v1.6.0 h1:Vw+CEXo8M/FZ1rb4EjcLv0gJqqw89b7+g+C/EmniTb8=
 cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ=
 cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc=
+cloud.google.com/go/orgpolicy v1.10.0 h1:XDriMWug7sd0kYT1QKofRpRHzjad0bK8Q8uA9q+XrU4=
 cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc=
 cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw=
+cloud.google.com/go/osconfig v1.11.0 h1:PkSQx4OHit5xz2bNyr11KGcaFccL5oqglFPdTboyqwQ=
 cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw=
 cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo=
+cloud.google.com/go/oslogin v1.9.0 h1:whP7vhpmc+ufZa90eVpkfbgzJRK/Xomjz+XCD4aGwWw=
 cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs=
 cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=
+cloud.google.com/go/phishingprotection v0.7.0 h1:l6tDkT7qAEV49MNEJkEJTB6vOO/onbSOcNtAT09HPuA=
 cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk=
 cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE=
+cloud.google.com/go/policytroubleshooter v1.6.0 h1:yKAGC4p9O61ttZUswaq9GAn1SZnEzTd0vUYXD7ZBT7Y=
 cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc=
 cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=
+cloud.google.com/go/privatecatalog v0.8.0 h1:EPEJ1DpEGXLDnmc7mnCAqFmkwUJbIsaLAiLHVOkkwtc=
 cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs=
 cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0=
+cloud.google.com/go/pubsub v1.30.0 h1:vCge8m7aUKBJYOgrZp7EsNDf6QMd2CAlXZqWTn3yq6s=
 cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4=
 cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg=
+cloud.google.com/go/pubsublite v1.7.0 h1:cb9fsrtpINtETHiJ3ECeaVzrfIVhcGjhhJEjybHXHao=
 cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM=
+cloud.google.com/go/recaptchaenterprise v1.3.1 h1:u6EznTGzIdsyOsvm+Xkw0aSuKFXQlyjGE9a4exk6iNQ=
+cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
 cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U=
+cloud.google.com/go/recaptchaenterprise/v2 v2.7.0 h1:6iOCujSNJ0YS7oNymI64hXsjGq60T4FK1zdLugxbzvU=
 cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c=
 cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=
+cloud.google.com/go/recommendationengine v0.7.0 h1:VibRFCwWXrFebEWKHfZAt2kta6pS7Tlimsnms0fjv7k=
 cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac=
 cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70=
+cloud.google.com/go/recommender v1.9.0 h1:ZnFRY5R6zOVk2IDS1Jbv5Bw+DExCI5rFumsTnMXiu/A=
 cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ=
 cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM=
+cloud.google.com/go/redis v1.11.0 h1:JoAd3SkeDt3rLFAAxEvw6wV4t+8y4ZzfZcZmddqphQ8=
 cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ=
 cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0=
+cloud.google.com/go/resourcemanager v1.7.0 h1:NRM0p+RJkaQF9Ee9JMnUV9BQ2QBIOq/v8M+Pbv/wmCs=
 cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI=
 cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg=
+cloud.google.com/go/resourcesettings v1.5.0 h1:8Dua37kQt27CCWHm4h/Q1XqCF6ByD7Ouu49xg95qJzI=
 cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA=
 cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y=
+cloud.google.com/go/retail v1.12.0 h1:1Dda2OpFNzIb4qWgFZjYlpP7sxX3aLeypKG6A3H4Yys=
 cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14=
 cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo=
+cloud.google.com/go/run v0.9.0 h1:ydJQo+k+MShYnBfhaRHSZYeD/SQKZzZLAROyfpeD9zw=
 cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg=
 cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44=
+cloud.google.com/go/scheduler v1.9.0 h1:NpQAHtx3sulByTLe2dMwWmah8PWgeoieFPpJpArwFV0=
 cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc=
 cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4=
+cloud.google.com/go/secretmanager v1.10.0 h1:pu03bha7ukxF8otyPKTFdDz+rr9sE3YauS5PliDXK60=
 cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU=
 cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA=
+cloud.google.com/go/security v1.13.0 h1:PYvDxopRQBfYAXKAuDpFCKBvDOWPWzp9k/H5nB3ud3o=
 cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0=
 cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk=
+cloud.google.com/go/securitycenter v1.19.0 h1:AF3c2s3awNTMoBtMX3oCUoOMmGlYxGOeuXSYHNBkf14=
 cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag=
 cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s=
+cloud.google.com/go/servicecontrol v1.11.1 h1:d0uV7Qegtfaa7Z2ClDzr9HJmnbJW7jn0WhZ7wOX6hLE=
+cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk=
 cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U=
+cloud.google.com/go/servicedirectory v1.9.0 h1:SJwk0XX2e26o25ObYUORXx6torSFiYgsGkWSkZgkoSU=
 cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s=
 cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo=
+cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkvi+gzu+NjywY=
+cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4=
 cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU=
+cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4=
+cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA=
 cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw=
+cloud.google.com/go/shell v1.6.0 h1:wT0Uw7ib7+AgZST9eCDygwTJn4+bHMDtZo5fh7kGWDU=
 cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A=
 cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos=
+cloud.google.com/go/spanner v1.45.0 h1:7VdjZ8zj4sHbDw55atp5dfY6kn1j9sam9DRNpPQhqR4=
 cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M=
 cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco=
+cloud.google.com/go/speech v1.15.0 h1:JEVoWGNnTF128kNty7T4aG4eqv2z86yiMJPT9Zjp+iw=
 cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI=
+cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
+cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
 cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=
+cloud.google.com/go/storagetransfer v1.8.0 h1:5T+PM+3ECU3EY2y9Brv0Sf3oka8pKmsCfpQ07+91G9o=
 cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw=
 cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA=
+cloud.google.com/go/talent v1.5.0 h1:nI9sVZPjMKiO2q3Uu0KhTDVov3Xrlpt63fghP9XjyEM=
 cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c=
 cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4=
+cloud.google.com/go/texttospeech v1.6.0 h1:H4g1ULStsbVtalbZGktyzXzw6jP26RjVGYx9RaYjBzc=
 cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc=
 cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg=
+cloud.google.com/go/tpu v1.5.0 h1:/34T6CbSi+kTv5E19Q9zbU/ix8IviInZpzwz3rsFE+A=
 cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM=
 cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y=
+cloud.google.com/go/trace v1.9.0 h1:olxC0QHC59zgJVALtgqfD9tGk0lfeCP5/AGXL3Px/no=
 cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk=
 cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg=
+cloud.google.com/go/translate v1.7.0 h1:GvLP4oQ4uPdChBmBaUSa/SaZxCdyWELtlAaKzpHsXdA=
 cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos=
 cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw=
+cloud.google.com/go/video v1.15.0 h1:upIbnGI0ZgACm58HPjAeBMleW3sl5cT84AbYQ8PWOgM=
 cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ=
 cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU=
+cloud.google.com/go/videointelligence v1.10.0 h1:Uh5BdoET8XXqXX2uXIahGb+wTKbLkGH7s4GXR58RrG8=
 cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU=
+cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4=
+cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=
 cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E=
+cloud.google.com/go/vision/v2 v2.7.0 h1:8C8RXUJoflCI4yVdqhTy9tRyygSHmp60aP363z23HKg=
 cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0=
 cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g=
+cloud.google.com/go/vmmigration v1.6.0 h1:Azs5WKtfOC8pxvkyrDvt7J0/4DYBch0cVbuFfCCFt5k=
 cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY=
+cloud.google.com/go/vmwareengine v0.3.0 h1:b0NBu7S294l0gmtrT0nOJneMYgZapr5x9tVWvgDoVEM=
 cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY=
 cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8=
+cloud.google.com/go/vpcaccess v1.6.0 h1:FOe6CuiQD3BhHJWt7E8QlbBcaIzVRddupwJlp7eqmn4=
 cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes=
 cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A=
+cloud.google.com/go/webrisk v1.8.0 h1:IY+L2+UwxcVm2zayMAtBhZleecdIFLiC+QJMzgb0kT0=
 cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg=
 cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ=
+cloud.google.com/go/websecurityscanner v1.5.0 h1:AHC1xmaNMOZtNqxI9Rmm87IJEyPaRkOxeI0gpAacXGk=
 cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng=
 cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=
+cloud.google.com/go/workflows v1.10.0 h1:FfGp9w0cYnaKZJhUOMqCOJCYT/WlvYBfTQhFWV3sRKI=
 cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw=
+contrib.go.opencensus.io/exporter/stackdriver v0.13.4 h1:ksUxwH3OD5sxkjzEqGxNTl+Xjsmu3BnC/300MhSVTSc=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krnnfGj4pV65YLUm3C0/8bl7V5Nry2Pwvy3ru/wLc=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
+github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4=
+github.com/Antonboom/errname v0.1.5 h1:IM+A/gz0pDhKmlt5KSNTVAvfLMb+65RxavBXpRtCUEg=
+github.com/Antonboom/nilnil v0.1.0 h1:DLDavmg0a6G/F4Lt9t7Enrbgb3Oph6LnDE6YVsmTt74=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
+github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
+github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8=
+github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
+github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
 github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
+github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE=
+github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8=
+github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3 h1:4FA+QBaydEHlwxg0lMN3rhwoDaQy6LKhVWR4qvq4BuA=
+github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
+github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
+github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 h1:WDC6ySpJzbxGWFh4aMxFFC28wwGp5pEuoTtvA4q/qQ4=
+github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
+github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
+github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
+github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
+github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg=
+github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
+github.com/alecthomas/kong v0.2.4 h1:Y0ZBCHAvHhTHw7FFJ2FzCAAG4pkbTgA45nc7BpMhDNk=
+github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
+github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae h1:AMzIhMUqU3jMrZiTuW0zkYeKlKDAFD+DG20IoO421/Y=
+github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
+github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
+github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
+github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed h1:ue9pVfIcP+QMEjfgo/Ez4ZjNZfonGgR6NgjMaJMu1Cg=
+github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
+github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
+github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM=
+github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
+github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY=
+github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
+github.com/apex/logs v1.0.0 h1:adOwhOTeXzZTnVuEK13wuJNBFutP0sOfutRS8NY+G6A=
+github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a h1:2KLQMJ8msqoPHIPDufkxVcoTtcmE5+1sL9950m4R9Pk=
+github.com/aphistic/sweet v0.2.0 h1:I4z+fAUqvKfvZV/CHi5dV0QuwbmIvYYFDjG0Ss5QpAs=
+github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
+github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
+github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
+github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
+github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
+github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=
+github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo=
+github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
+github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
+github.com/ashanbrown/forbidigo v1.2.0 h1:RMlEFupPCxQ1IogYOQUnIQwGEUGK8g5vAPMRyJoSxbc=
+github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde h1:YOsoVXsZQPA9aOTy1g0lAJv5VzZUvwQuZqug8XPeqfM=
+github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns=
+github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg=
+github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
 github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
+github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
+github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU=
+github.com/bkielbasa/cyclop v1.2.0 h1:7Jmnh0yL2DjKfw28p86YTd/B4lRGcNuu12sKE35sM7A=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/blizzy78/varnamelen v0.3.0 h1:80mYO7Y5ppeEefg1Jzu+NBg16iwToOQVnDnNIoWSShs=
+github.com/bombsimon/wsl/v3 v3.3.0 h1:Mka/+kRLoQJq7g2rggtgQsjuI/K5Efd87WX96EWFxjM=
+github.com/breml/bidichk v0.1.1 h1:Qpy8Rmgos9qdJxhka0K7ADEE5bQZX9PQUthkgggHpFM=
+github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
 github.com/bufbuild/connect-go v1.5.2 h1:G4EZd5gF1U1ZhhbVJXplbuUnfKpBZ5j5izqIwu2g2W8=
+github.com/bufbuild/connect-go v1.5.2/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/butuzov/ireturn v0.1.1 h1:QvrO2QF2+/Cx1WA/vETCIYBKtRjc30vesdoPUNo1EbY=
+github.com/bytecodealliance/wasmtime-go v0.36.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI=
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
 github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
+github.com/charithe/durationcheck v0.0.9 h1:mPP4ucLrf/rKZiIG/a9IPXHGlh8p4CzgpyTy6EEutYk=
+github.com/charmbracelet/glamour v0.3.0 h1:3H+ZrKlSg8s+WU6V7eF2eRVYt8lCueffbi7r2+ffGkc=
+github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af h1:spmv8nSH9h5oCQf40jt/ufBCt9j0/58u4G+rkeMqXGI=
+github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo=
+github.com/checkpoint-restore/go-criu/v5 v5.3.0 h1:wpFFOoomK3389ue2lAb0Boag6XPht5QYpipxmSNL4d8=
+github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM=
+github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI=
+github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ=
+github.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k=
+github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY=
+github.com/cli/oauth v0.8.0 h1:YTFgPXSTvvDUFti3tR4o6q7Oll2SnQ9ztLwCAn4/IOA=
+github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
+github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk=
 github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
 github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
+github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
+github.com/containerd/aufs v1.0.0 h1:2oeJiwX5HstO7shSrPZjrohJZLzK36wvpdmzDRkL/LY=
+github.com/containerd/btrfs v1.0.0 h1:osn1exbzdub9L5SouXO5swW4ea/xVdJZ3wokxN5GrnA=
+github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
+github.com/containerd/continuity v0.2.3-0.20220330195504-d132b287edc8/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk=
+github.com/containerd/fifo v1.0.0 h1:6PirWBr9/L7GDamKr+XM0IeUFXu5mf3M/BPpH9gaLBU=
+github.com/containerd/fuse-overlayfs-snapshotter v1.0.2 h1:Xy9Tkx0tk/SsMfLDFc69wzqSrxQHYEFELHBO/Z8XO3M=
+github.com/containerd/fuse-overlayfs-snapshotter v1.0.2/go.mod h1:nRZceC8a7dRm3Ao6cJAwuJWPFiBPaibHiFntRUnzhwU=
+github.com/containerd/go-cni v1.1.6 h1:el5WPymG5nRRLQF1EfB97FWob4Tdc8INg8RZMaXWZlo=
+github.com/containerd/go-cni v1.1.6/go.mod h1:BWtoWl5ghVymxu6MBjg79W9NZrCRyHIdUtk4cauMe34=
+github.com/containerd/go-runc v1.0.0 h1:oU+lLv1ULm5taqgV/CJivypVODI4SUz1znWjv3nNYS0=
+github.com/containerd/imgcrypt v1.1.4 h1:iKTstFebwy3Ak5UF0RHSeuCTahC5OIrPJa6vjMAM81s=
+github.com/containerd/imgcrypt v1.1.4/go.mod h1:LorQnPtzL/T0IyCeftcsMEO7AqxUDbdO8j/tSUpgxvo=
+github.com/containerd/nri v0.1.0 h1:6QioHRlThlKh2RkRTR4kIT3PKAcrLo3gIWnjkM4dQmQ=
 github.com/containerd/stargz-snapshotter v0.11.3 h1:D3PoF563XmOBdtfx2G6AkhbHueqwIVPBFn2mrsWLa3w=
+github.com/containerd/stargz-snapshotter v0.11.3/go.mod h1:2j2EAUyvrLU4D9unYlTIwGhDKQIk74KJ9E71lJsQCVM=
+github.com/containerd/ttrpc v1.1.0 h1:GbtyLRxb0gOLR0TYQWt3O6B0NvT8tMdorEHqIQo/lWI=
+github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY=
+github.com/containerd/zfs v1.0.0 h1:cXLJbx+4Jj7rNsTiqVfm6i+RNLx6FFA2fMmDlEf+Wm8=
+github.com/containernetworking/cni v1.1.1 h1:ky20T7c0MvKvbMOwS/FrlbNwjEoqJEUUYfsL4b0mc4k=
+github.com/containernetworking/cni v1.1.1/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw=
+github.com/containernetworking/plugins v1.1.1 h1:+AGfFigZ5TiQH00vhR8qPeSatj53eNGz0C1d3wVYlHE=
+github.com/containernetworking/plugins v1.1.1/go.mod h1:Sr5TH/eBsGLXK/h71HeLfX19sZPp3ry5uHSkI4LPxV8=
+github.com/containers/ocicrypt v1.1.3 h1:uMxn2wTb4nDR7GqG3rnZSfpJXqWURfzZ7nKydzIeKpA=
+github.com/containers/ocicrypt v1.1.3/go.mod h1:xpdkbVAuaH3WzbEabUd5yDsl9SwJA5pABH85425Es2g=
+github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
+github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ=
+github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo=
+github.com/coreos/go-iptables v0.5.0 h1:mw6SAibtHKZcNzAsOxjoHIG0gy5YFHhypWSSNc6EjbQ=
+github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
+github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
+github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
+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/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/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=
+github.com/denis-tingajkin/go-header v0.4.2 h1:jEeSF4sdv8/3cT/WY8AgDHUoItNSoEZ7qg9dX7pc218=
+github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk=
+github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4=
+github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
+github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4=
+github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4=
+github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
+github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o=
 github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
 github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA=
 github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q=
 github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
+github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8=
 github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
+github.com/esimonov/ifshort v1.0.3 h1:JD6x035opqGec5fZ0TLjXeROD2p5H7oLGn8MKfy9HTM=
+github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM=
+github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw=
+github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc=
+github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
+github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
+github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
+github.com/flowstack/go-jsonschema v0.1.1 h1:dCrjGJRXIlbDsLAgTJZTjhwUJnnxVWl1OgNyYh5nyDc=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
+github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
+github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
+github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
+github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
+github.com/fullstorydev/grpcurl v1.6.0 h1:p8BB6VZF8O7w6MxGr3KJ9E6EVKaswCevSALK6FBtMzA=
+github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
+github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
+github.com/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc=
+github.com/gabriel-vasile/mimetype v1.1.2 h1:gaPnPcNor5aZSVCJVSGipcpbgMWiAAj9z182ocSGbHU=
+github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko=
+github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
+github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
+github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
+github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
+github.com/go-critic/go-critic v0.6.1 h1:lS8B9LH/VVsvQQP7Ao5TJyQqteVKVs3E4dXiHMyubtI=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
+github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
+github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
+github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
+github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk=
+github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc=
 github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o=
+github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
+github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
+github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
+github.com/go-toolsmith/astequal v1.0.1 h1:JbSszi42Jiqu36Gnf363HWS9MTEAz67vTQLponh3Moc=
+github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
+github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21 h1:wP6mXeB2V/d1P1K7bZ5vDUO3YqEzcvOREOxZPEu3gVI=
+github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
+github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg=
+github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
+github.com/go-toolsmith/typep v1.0.2 h1:8xdsa1+FSIH/RhEkgnD1j2CJOy5mNllW1Q9tRiYwvlk=
+github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
+github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
+github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
+github.com/godror/godror v0.24.2 h1:uxGAD7UdnNGjX5gf4NnEIGw0JAPTIFiqAyRBZTPKwXs=
+github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
+github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
 github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
+github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
+github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
+github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a h1:iR3fYXUjHCR97qWS8ch1y9zPNsgXThGwjKPrYfqMPks=
+github.com/golangci/golangci-lint v1.43.0 h1:SLwZFEmDgopqZpfP495zCtV9REUf551JJlJ51Ql7NZA=
+github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA=
+github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
+github.com/golangci/misspell v0.3.5 h1:pLzmVdl3VxTOncgzHcvLOKirdvcx/TydsClUQXTehjo=
+github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 h1:SgM7GDZTxtTTQPU84heOxy34iG5Du7F2jcoZnvp+fXI=
+github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
+github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
+github.com/google/cel-go v0.12.5 h1:DmzaiSgoaqGCjtpPQWl26/gND+yRpim56H1jCVev6d8=
+github.com/google/cel-go v0.12.5/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw=
+github.com/google/certificate-transparency-go v1.1.1 h1:6JHXZhXEvilMcTjR4MGZn5KV0IRkcFl4CJx5iHVhjFE=
+github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM=
+github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
+github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
+github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
+github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
+github.com/google/trillian v1.3.11 h1:pPzJPkK06mvXId1LHEAJxIegGgHzzp/FUnycPYfoCMI=
+github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
+github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254 h1:Nb2aRlC404yz7gQIfRZxX9/MLvQiqXyiBTJtgAy6yrI=
+github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY=
+github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
+github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
+github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
+github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q=
+github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5 h1:rx8127mFPqXXsfPSo8BwnIU97MKFZc89WHAHt8PwDVY=
+github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk=
+github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY=
+github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
+github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
 github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d h1:ibbzF2InxMOS+lLCphY9PHNKPURDUBNKaG6ErSq8gJQ=
+github.com/hanwen/go-fuse/v2 v2.1.1-0.20220112183258-f57e95bda82d/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc=
+github.com/hashicorp/consul/api v1.11.0 h1:Hw/G8TtRvOElqxVIhBzXciiSTbapq8hZ2XKZsXk5ZCE=
+github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
+github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU=
 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
+github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
+github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
+github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
+github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 h1:PFfGModn55JA0oBsvFghhj0v93me+Ctr3uHC/UmFAls=
+github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0=
+github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
+github.com/hashicorp/mdns v1.0.1 h1:XFSOubp8KWB+Jd2PDyaX5xUd5bhSP/+pTDZVDMzZJM8=
+github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g=
+github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc=
+github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
+github.com/hashicorp/terraform-exec v0.15.0 h1:cqjh4d8HYNQrDoEmlSGelHmg2DYDh5yayckvJ5bV18E=
+github.com/hashicorp/terraform-exec v0.15.0/go.mod h1:H4IG8ZxanU+NW0ZpDRNsvh9f0ul7C0nHP+rUR/CHs7I=
+github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY=
+github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk=
+github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 h1:S4qyfL2sEm5Budr4KVMyEniCy+PbS55651I/a+Kn/NQ=
+github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E=
+github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2 h1:rcanfLhLDA8nozr/K289V1zcntHr3V+SHlXwzz1ZI2g=
+github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
+github.com/intel/goresctrl v0.2.0 h1:JyZjdMQu9Kl/wLXe9xA6s1X+tF6BWsQPFGJMEeCfWzE=
+github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
+github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4=
+github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE=
+github.com/iris-contrib/jade v1.1.3 h1:p7J/50I0cjo0wq/VWVCDFd8taPJbuFC+bq23SniRFX0=
+github.com/iris-contrib/pongo2 v0.0.1 h1:zGP7pW51oi5eQZMIlGA3I+FHY9/HOQWDB+572yin0to=
+github.com/iris-contrib/schema v0.0.1 h1:10g/WnoRR+U+XXHWKBHeNy/+tZmM2kcAVGLOsz+yaDA=
+github.com/ishidawataru/sctp v0.0.0-20210226210310-f2269e66cdee h1:PAXLXk1heNZ5yokbMBpVLZQxo43wCZxRwl00mX+dd44=
+github.com/ishidawataru/sctp v0.0.0-20210226210310-f2269e66cdee/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
+github.com/itchyny/go-flags v1.5.0 h1:Z5q2ist2sfDjDlExVPBrMqlsEDxDR2h4zuOElB0OEYI=
+github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56 h1:742eGXur0715JMq73aD95/FU0XpVKXqNuTnEfXsLOYQ=
 github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
 github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
+github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
+github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
+github.com/jgautheron/goconst v1.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM=
+github.com/jhump/protoreflect v1.6.1 h1:4/2yi5LyDPP7nN+Hiird1SAJ6YoxUm13/oxHGRnbPd8=
+github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs=
+github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48=
+github.com/joefitzgerald/rainbow-reporter v0.1.0 h1:AuMG652zjdzI0YCCnXAqATtRBpGXMcAnrajcaTrSeuo=
+github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
+github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a h1:8NZHLa6Gp0hW6xJ0c3F1Kse7dJw30fOcDzHuF9sLbnE=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
+github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d h1:XeSMXURZPtUffuWAaq90o6kLgZdgu+QA8wk4MPC8ikI=
+github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
+github.com/kataras/golog v0.0.10 h1:vRDRUmwacco/pmBAm8geLn8rHEdc+9Z4NAr5Sh7TG/4=
+github.com/kataras/iris/v12 v12.1.8 h1:O3gJasjm7ZxpxwTH8tApZsvf274scSGQAUpNe47c37U=
+github.com/kataras/neffos v0.0.14 h1:pdJaTvUG3NQfeMbbVCI8JT2T5goPldyyfUB2PJfh1Bs=
+github.com/kataras/pio v0.0.2 h1:6NAi+uPJ/Zuid6mrAKlgpbI11/zK/lV4B2rxWaJN98Y=
+github.com/kataras/sitemap v0.0.5 h1:4HCONX5RLgVy6G4RkYOV3vKNcma9p236LdGOipJsaFE=
+github.com/kisielk/errcheck v1.6.0 h1:YTDO4pNy7AUN/021p+JGHycQyYNIyMoenM1YDVK6RlY=
+github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
+github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
+github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
+github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
+github.com/kortschak/utter v1.0.1 h1:AJVccwLrdrikvkH0aI5JKlzZIORLpfMeGBQ5tHfIXis=
+github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kulti/thelper v0.4.0 h1:2Nx7XbdbE/BYZeoip2mURKUdtHQRuy6Ug+wR7K9ywNM=
+github.com/kunwardeep/paralleltest v1.0.3 h1:UdKIkImEAXjR1chUWLn+PNXqWUGs//7tzMeWuP7NhmI=
+github.com/kyoh86/exportloopref v0.1.8 h1:5Ry/at+eFdkX9Vsdw3qU4YkvGtzuVfzT4X7S77LoN/M=
+github.com/labstack/echo/v4 v4.1.11 h1:z0BZoArY4FqdpUEl+wlHp4hnr/oSR6MTmQmv8OHSoww=
+github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
+github.com/launchdarkly/go-ntlm-proxy-auth v1.0.1 h1:Iz5cg9mB/0vt5llZE+J0iGQ5+O/U8CWuRUUR7Ou8eNM=
+github.com/launchdarkly/go-ntlm-proxy-auth v1.0.1/go.mod h1:hKWfH/hga5oslM2mRkDZi+14u2h1dFsmgbvSM9qF8pk=
+github.com/launchdarkly/go-ntlmssp v1.0.1 h1:snB77118TQvf9tfHrkSyrIop/UX5e5VD2D2mv7Kh3wE=
+github.com/launchdarkly/go-ntlmssp v1.0.1/go.mod h1:/cq3t2JyALD7GdVF5BEWcEuGlIGa44FZ4v4CVk7vuCY=
+github.com/launchdarkly/go-test-helpers/v3 v3.0.2/go.mod h1:u2ZvJlc/DDJTFrshWW50tWMZHLVYXofuSHUfTU/eIwM=
+github.com/ldez/gomoddirectives v0.2.2 h1:p9/sXuNFArS2RLc+UpYZSI4KQwGMEDWC/LbtF5OPFVg=
+github.com/ldez/tagliatelle v0.2.0 h1:693V8Bf1NdShJ8eu/s84QySA0J2VWBanVBa2WwXD/Wk=
+github.com/letsencrypt/pkcs11key/v4 v4.0.0 h1:qLc/OznH7xMr5ARJgkZCCWk+EomQkiNTOoOF5LAgagc=
+github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3 h1:jUp75lepDg0phMUJBCmvaeFDldD2N3S1lBuPwUTszio=
+github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
+github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
+github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
 github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
+github.com/lyft/protoc-gen-star/v2 v2.0.1 h1:keaAo8hRuAT0O3DfJ/wM3rufbAjGeJ1lAtWZHDjKGB0=
+github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
+github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
+github.com/maratori/testpackage v1.0.1 h1:QtJ5ZjqapShm0w5DosRjg0PRlSdAdlx+W6cCKoALdbQ=
+github.com/marstr/guid v1.1.0 h1:/M4H/1G4avsieL6BbUwCOBzulmoeKVP5ux/3mQNnbyI=
+github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 h1:pWxk9e//NbPwfxat7RXkts09K+dEBJWakUWwICVqYbA=
+github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM=
+github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
+github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
+github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc=
+github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2 h1:g+4J5sZg6osfvEfkRZxJ1em0VT95/UOZgi/l7zi1/oE=
+github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo=
+github.com/mediocregopher/radix/v3 v3.4.2 h1:galbPBjIwmyREgwGCfQEN4X8lxbJnKBYurgz+VfcStA=
+github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 h1:zpIH83+oKzcpryru8ceC6BxnoG8TBrhgAvRg8obzup0=
+github.com/mgechev/revive v1.1.2 h1:MiYA/o9M7REjvOF20QN43U8OtXDDHQFKLCtJnxLGLog=
+github.com/microcosm-cc/bluemonday v1.0.6 h1:ZOvqHKtnx0fUpnbQm3m3zKFWE+DRC+XB1onh8JoEObE=
+github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
+github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
+github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
+github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
+github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
+github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
+github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
+github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible h1:aKW/4cBs+yK6gpqU3K/oIwk9Q/XICqd3zOX/UFuvqmk=
+github.com/mitchellh/cli v1.1.4 h1:qj8czE26AU4PbiaPXK5uVmMSM+V5BYsFBiM9HhGRLUA=
+github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
+github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc=
+github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
+github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
+github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
 github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/51gbeLHfKGNfgEQQIWrlbdaOsidbQ=
+github.com/moby/sys/signal v0.6.0 h1:aDpY94H8VlhTGa9sNYUFCFsMZIUh5wm0B6XkIoJj/iY=
+github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
+github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc=
+github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
+github.com/moricho/tparallel v0.2.1 h1:95FytivzT6rYzdJLdtfn6m1bfFJylOJK41+lgv/EHf4=
+github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
+github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1 h1:29NKShH4TWd3lxCDUhS4Xe16EWMA753dtIxYtwddklU=
+github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5 h1:0KqC6/sLy7fDpBdybhVkkv4Yz+PmB7c9Dz9z3dLW804=
+github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4=
+github.com/muesli/reflow v0.2.0 h1:2o0UBJPHHH4fa2GCXU4Rg4DwOtWPMekCeyc5EWbAQp0=
+github.com/muesli/termenv v0.8.1 h1:9q230czSP3DHVpkaPDXGp0TOfAwyjyYwXlUCQxQSaBk=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
+github.com/mwitkow/go-proto-validators v0.2.0 h1:F6LFfmgVnfULfaRsQWBbe7F7ocuHCr9+7m+GAeDzNbQ=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
+github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
+github.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
 github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ=
 github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4=
+github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
+github.com/ncw/swift v1.0.47 h1:4DQRPj35Y41WogBxyhOXlrI37nzGlyEcsforeudyYPQ=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/nishanths/exhaustive v0.2.3 h1:+ANTMqRNrqwInnP9aszg/0jDo+zbXa4x66U19Bx/oTk=
+github.com/nishanths/predeclared v0.2.1 h1:1TXtjmy4f3YCFjTxRd8zcFHOmoUir+gp0ESzjFzG2sw=
+github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
+github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc=
+github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39 h1:H7DMc6FAjgwZZi8BRqjrAAHWoqEr5e5L6pS4V0ezet4=
+github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
+github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k=
+github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
+github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
 github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
+github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
+github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d h1:zapSxdmZYY6vJWXFKLQ+MkI+agc+HQyfrCGowDSHiKs=
+github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
+github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d h1:CdDQnGF8Nq9ocOS/xlSptM1N3BbrA6/kmaep5ggwaIA=
+github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
+github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
+github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
+github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
+github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
+github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
+github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349 h1:Kq/3kL0k033ds3tyez5lFPrfQ74fNJ+OqCclRipubwA=
 github.com/porter-dev/api-contracts v0.0.63/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
 github.com/porter-dev/api-contracts v0.0.86/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
+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/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=
+github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c h1:JoUA0uz9U0FVFq5p4LjEq4C0VgQ0El320s3Ms0V4eww=
+github.com/quasilyte/go-ruleguard v0.3.13 h1:O1G41cq1jUr3cJmqp7vOUT0SokqjzmS9aESWJuIDRaY=
+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/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/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/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 h1:2c1EFnZHIPCW8qKWgHMH/fX2PkSabFc5mrVzfUNdg5U=
+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/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
+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.2-0.20210429002308-3879420cc921 h1:58EBmR2dMNL2n/FnbQewK3D14nXr0V9CObDSvMJLq+Y=
+github.com/securego/gosec/v2 v2.9.1 h1:anHKLS/ApTYU6NZkKa/5cQqqcbKZURjvc+MtR++S4EQ=
+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/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU=
+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/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/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
+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/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/gunit v1.0.0 h1:RyPDUFcJbvtXlhJPk7v+wnxZRY2EUokhEYl2EJOPToI=
+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/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I=
+github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
+github.com/sylvia7788/contextcheck v1.0.4 h1:MsiVqROAdr0efZc/fOCt0c235qm9XJqHtWwM+2h2B04=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
 github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck=
+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/go-buffer v1.1.0 h1:Lo2OsPHlIxXF24zApe15AbK3bJLAOvkkxEA6Ux4c47M=
+github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2 h1:eGaGNxrtoZf/mBURsnNQKDR7u50Klgcf2eFDQEnc8Bc=
+github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b h1:m74UWYy+HBs+jMFR9mdZU6shPewugMyH5+GV6LNgW8w=
+github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
+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=
+github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
+github.com/tommy-muehle/go-mnd/v2 v2.4.0 h1:1t0f8Uiaq+fqKteUR4N9Umr6E99R+lDnLnq7PwX2PPE=
+github.com/tonistiigi/fsutil v0.0.0-20220115021204-b19f7f9cb274 h1:wbyZxD6IPFp0sl5uscMOJRsz5UKGFiNiD16e+MVfKZY=
+github.com/tonistiigi/fsutil v0.0.0-20220115021204-b19f7f9cb274/go.mod h1:oPAfvw32vlUJSjyDcQ3Bu0nb2ON2B+G0dtVN/SZNJiA=
+github.com/tonistiigi/go-actions-cache v0.0.0-20220404170428-0bdeb6e1eac7 h1:8eY6m1mjgyB8XySUR7WvebTM8D/Vs86jLJzD/Tw7zkc=
+github.com/tonistiigi/go-actions-cache v0.0.0-20220404170428-0bdeb6e1eac7/go.mod h1:qqvyZqkfwkoJuPU/bw61bItaoO0SJ8YSW0vSVRRvsRg=
+github.com/tonistiigi/go-archvariant v1.0.0 h1:5LC1eDWiBNflnTF1prCiX09yfNHIxDC/aukdhCdTyb0=
+github.com/tonistiigi/go-archvariant v1.0.0/go.mod h1:TxFmO5VS6vMq2kvs3ht04iPXtu2rUT/erOnGFYfk5Ho=
+github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
+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.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
+github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
+github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
+github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
+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.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g=
+github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
+github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
+github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
+github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 h1:EVObHAr8DqpoJCVv6KYTle8FEImKhtkfcZetNqxDoJQ=
+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-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 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
+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/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
+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.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
+github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
+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.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
+go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo=
+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=
+go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao=
+go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
+go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4=
+go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
+go.etcd.io/etcd/pkg/v3 v3.5.4 h1:V5Dvl7S39ZDwjkKqJG2BfXgxZ3QREqqKifWQgIw5IM0=
+go.etcd.io/etcd/pkg/v3 v3.5.4/go.mod h1:OI+TtO+Aa3nhQSppMbwE4ld3uF1/fqqwbpfndbbrEe0=
+go.etcd.io/etcd/raft/v3 v3.5.4 h1:YGrnAgRfgXloBNuqa+oBI/aRZMcK/1GS6trJePJ/Gqc=
+go.etcd.io/etcd/raft/v3 v3.5.4/go.mod h1:SCuunjYvZFC0fBX0vxMSPjuZmpcSk+XaAcMrD6Do03w=
+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.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
+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/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/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/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/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw=
+go.opentelemetry.io/otel/sdk/export/metric v0.20.0 h1:c5VRjxCXdQlx1HjzwGdQHzZaVI82b5EbBgOu2ljD92g=
+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/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/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.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-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -283,7 +992,11 @@ golang.org/x/sys v0.6.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/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=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA=
 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=
@@ -294,8 +1007,63 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
 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/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=
+gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
+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/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/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
+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/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+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/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
 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=
+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-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.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-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHzPMyB0t8BaFeBYI=
+k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
+k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8=
+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=
+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=
+modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
+modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
+modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
+modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
+modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
+mvdan.cc/gofumpt v0.1.1 h1:bi/1aS/5W00E2ny5q65w9SnKpWEF/UIOqDYBILpo9rA=
+mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
+mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
+mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7 h1:HT3e4Krq+IE44tiN36RvVEb6tvqeIdtsVSsxmNPqlFU=
+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.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=

+ 47 - 0
internal/features/launch_darkly.go

@@ -0,0 +1,47 @@
+package features
+
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
+	ld "github.com/launchdarkly/go-server-sdk/v6"
+	"github.com/porter-dev/porter/api/server/shared/config/envloader"
+)
+
+// Client is a struct wrapper around the launchdarkly client
+type Client struct {
+	client *ld.LDClient
+}
+
+// BoolVariation returns the value of a boolean feature flag for a given evaluation context.
+//
+// Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and
+// has no off variation.
+//
+// For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
+func (c Client) BoolVariation(field string, context ldcontext.Context, defaultValue bool) (bool, error) {
+	if c.client == nil {
+		return defaultValue, errors.New("failed to participate in launchdarkly test: no client available")
+	}
+	return c.client.BoolVariation(field, context, defaultValue)
+}
+
+// GetClient retrieves a Client for interacting with LaunchDarkly
+func GetClient(envConf *envloader.EnvConf) (*Client, error) {
+	ldClient, err := ld.MakeClient(envConf.ServerConf.LaunchDarklySDKKey, 5*time.Second)
+	if err != nil {
+		return &Client{}, fmt.Errorf("failed to create new launchdarkly client: %w", err)
+	}
+
+	if ldClient == nil {
+		return &Client{}, errors.New("failed to create new launchdarkly client: invalid config")
+	}
+
+	if !ldClient.Initialized() {
+		return &Client{}, errors.New("failed to create new launchdarkly client: sdk failed to initialize")
+	}
+
+	return &Client{ldClient}, nil
+}

+ 1 - 1
internal/kubernetes/config.go

@@ -400,7 +400,7 @@ func (conf *OutOfClusterConfig) GetClientConfigFromCluster(ctx context.Context)
 
 func (conf *OutOfClusterConfig) CreateRawConfigFromCluster(ctx context.Context) (*api.Config, error) {
 	ctx, span := telemetry.NewSpan(ctx, "ooc-create-raw-config-from-cluster")
-	defer span.End()
+	// defer span.End() // This span is one of most frequent spans. We need to sample this. For now, this span will not send
 
 	cluster := conf.Cluster
 

+ 96 - 0
internal/models/feature_flag.go

@@ -0,0 +1,96 @@
+package models
+
+import (
+	"fmt"
+
+	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
+	"github.com/porter-dev/porter/internal/features"
+)
+
+func getAPITokensEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("api_tokens_enabled", context, defaultValue)
+	return value
+}
+
+func getAzureEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("azure_enabled", context, defaultValue)
+	return value
+}
+
+func getCapiProvisionerEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := true
+	value, _ := client.BoolVariation("capi_provisioner_enabled", context, defaultValue)
+	return value
+}
+
+func getEnableReprovision(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("enable_reprovision", context, defaultValue)
+	return value
+}
+
+func getFullAddOns(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("full_add_ons", context, defaultValue)
+	return value
+}
+
+func getHelmValuesEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("helm_values_enabled", context, defaultValue)
+	return value
+}
+
+func getManagedInfraEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("managed_infra_enabled", context, defaultValue)
+	return value
+}
+
+func getMultiCluster(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("multi_cluster", context, defaultValue)
+	return value
+}
+
+func getPreviewEnvsEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("preview_envs_enabled", context, defaultValue)
+	return value
+}
+
+func getRdsDatabasesEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("rds_databases_enabled", context, defaultValue)
+	return value
+}
+
+func getSimplifiedViewEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := true
+	value, _ := client.BoolVariation("simplified_view_enabled", context, defaultValue)
+	return value
+}
+
+func getStacksEnabled(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("stacks_enabled", context, defaultValue)
+	return value
+}
+
+func getValidateApplyV2(context ldcontext.Context, client *features.Client) bool {
+	defaultValue := false
+	value, _ := client.BoolVariation("validate_apply_v2", context, defaultValue)
+	return value
+}
+
+func getProjectContext(projectID uint, projectName string) ldcontext.Context {
+	projectIdentifier := fmt.Sprintf("project-%d", projectID)
+	launchDarklyName := fmt.Sprintf("%s: %s", projectIdentifier, projectName)
+	return ldcontext.NewBuilder(projectIdentifier).
+		Kind("project").
+		Name(launchDarklyName).
+		SetInt("project_id", int(projectID)).
+		Build()
+}

+ 25 - 19
internal/models/project.go

@@ -4,6 +4,7 @@ import (
 	"gorm.io/gorm"
 
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/features"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 )
 
@@ -18,7 +19,7 @@ const (
 
 // Project type that extends gorm.Model
 type Project struct {
-	gorm.Model
+	gorm.Model `gorm:"embedded"`
 
 	Name  string `json:"name"`
 	Roles []Role `json:"roles"`
@@ -74,30 +75,35 @@ type Project struct {
 }
 
 // ToProjectType generates an external types.Project to be shared over REST
-func (p *Project) ToProjectType() *types.Project {
+func (p *Project) ToProjectType(launchDarklyClient *features.Client) types.Project {
 	roles := make([]*types.Role, 0)
 
 	for _, role := range p.Roles {
 		roles = append(roles, role.ToRoleType())
 	}
 
-	return &types.Project{
-		ID:                     p.ID,
-		Name:                   p.Name,
-		Roles:                  roles,
-		PreviewEnvsEnabled:     p.PreviewEnvsEnabled,
-		RDSDatabasesEnabled:    p.RDSDatabasesEnabled,
-		ManagedInfraEnabled:    p.ManagedInfraEnabled,
-		StacksEnabled:          p.StacksEnabled,
-		APITokensEnabled:       p.APITokensEnabled,
-		CapiProvisionerEnabled: p.CapiProvisionerEnabled,
-		SimplifiedViewEnabled:  p.SimplifiedViewEnabled,
-		AzureEnabled:           p.AzureEnabled,
-		HelmValuesEnabled:      p.HelmValuesEnabled,
-		MultiCluster:           p.MultiCluster,
-		EnableReprovision:      p.EnableReprovision,
-		ValidateApplyV2:        p.ValidateApplyV2,
-		FullAddOns:             p.FullAddOns,
+	projectID := p.ID
+	projectName := p.Name
+	ldContext := getProjectContext(projectID, projectName)
+
+	return types.Project{
+		ID:    projectID,
+		Name:  projectName,
+		Roles: roles,
+
+		PreviewEnvsEnabled:     getPreviewEnvsEnabled(ldContext, launchDarklyClient),
+		RDSDatabasesEnabled:    getRdsDatabasesEnabled(ldContext, launchDarklyClient),
+		ManagedInfraEnabled:    getManagedInfraEnabled(ldContext, launchDarklyClient),
+		StacksEnabled:          getStacksEnabled(ldContext, launchDarklyClient),
+		APITokensEnabled:       getAPITokensEnabled(ldContext, launchDarklyClient),
+		CapiProvisionerEnabled: getCapiProvisionerEnabled(ldContext, launchDarklyClient),
+		SimplifiedViewEnabled:  getSimplifiedViewEnabled(ldContext, launchDarklyClient),
+		AzureEnabled:           getAzureEnabled(ldContext, launchDarklyClient),
+		HelmValuesEnabled:      getHelmValuesEnabled(ldContext, launchDarklyClient),
+		MultiCluster:           getMultiCluster(ldContext, launchDarklyClient),
+		EnableReprovision:      getEnableReprovision(ldContext, launchDarklyClient),
+		ValidateApplyV2:        getValidateApplyV2(ldContext, launchDarklyClient),
+		FullAddOns:             getFullAddOns(ldContext, launchDarklyClient),
 	}
 }
 

+ 18 - 3
internal/porter_app/parse.go

@@ -3,6 +3,7 @@ package porter_app
 import (
 	"context"
 
+	v1 "github.com/porter-dev/porter/internal/porter_app/v1"
 	v2 "github.com/porter-dev/porter/internal/porter_app/v2"
 
 	"sigs.k8s.io/yaml"
@@ -17,15 +18,17 @@ type PorterYamlVersion string
 const (
 	// PorterYamlVersion_V2 is the v2 version of the porter yaml
 	PorterYamlVersion_V2 PorterYamlVersion = "v2"
+	// PorterYamlVersion_V1 is the v1, legacy version of the porter yaml
+	PorterYamlVersion_V1 PorterYamlVersion = "v1stack"
 )
 
 // ParseYAML converts a Porter YAML file into a PorterApp proto object
-func ParseYAML(ctx context.Context, porterYaml []byte) (*porterv1.PorterApp, error) {
+func ParseYAML(ctx context.Context, porterYaml []byte, appName string) (*porterv1.PorterApp, error) {
 	ctx, span := telemetry.NewSpan(ctx, "porter-app-parse-yaml")
 	defer span.End()
 
 	if porterYaml == nil {
-		return nil, telemetry.Error(ctx, span, nil, "porter yaml is nil")
+		return nil, telemetry.Error(ctx, span, nil, "porter yaml input is nil")
 	}
 
 	version := &yamlVersion{}
@@ -35,18 +38,30 @@ func ParseYAML(ctx context.Context, porterYaml []byte) (*porterv1.PorterApp, err
 	}
 
 	var appProto *porterv1.PorterApp
+
 	switch version.Version {
 	case PorterYamlVersion_V2:
 		appProto, err = v2.AppProtoFromYaml(ctx, porterYaml)
 		if err != nil {
 			return nil, telemetry.Error(ctx, span, err, "error converting v2 yaml to proto")
 		}
+	// backwards compatibility for old porter.yaml files
+	// track this span in telemetry and reach out to customers who are still using old porter.yaml if they exist.
+	// once no one is converting from old porter.yaml, we can remove this code
+	case PorterYamlVersion_V1, "":
+		if appName == "" {
+			return nil, telemetry.Error(ctx, span, nil, "v1 porter yaml requires externally-provided app name")
+		}
+		appProto, err = v1.AppProtoFromYaml(ctx, porterYaml, appName)
+		if err != nil {
+			return nil, telemetry.Error(ctx, span, err, "error converting v1 yaml to proto")
+		}
 	default:
 		return nil, telemetry.Error(ctx, span, nil, "porter yaml version not supported")
 	}
 
 	if appProto == nil {
-		return nil, telemetry.Error(ctx, span, nil, "porter yaml is nil")
+		return nil, telemetry.Error(ctx, span, nil, "porter yaml output is nil")
 	}
 
 	return appProto, nil

+ 77 - 1
internal/porter_app/parse_test.go

@@ -20,6 +20,7 @@ func TestParseYAML(t *testing.T) {
 		want               *porterv1.PorterApp
 	}{
 		{"v2_input_nobuild", result_nobuild},
+		{"v1_input_no_build_no_image", v1_result_nobuild_no_image},
 	}
 
 	for _, tt := range tests {
@@ -29,7 +30,7 @@ func TestParseYAML(t *testing.T) {
 			want, err := os.ReadFile(fmt.Sprintf("testdata/%s.yaml", tt.porterYamlFileName))
 			is.NoErr(err) // no error expected reading test file
 
-			got, err := ParseYAML(context.Background(), want)
+			got, err := ParseYAML(context.Background(), want, "test-app")
 			is.NoErr(err) // umbrella chart values should convert to map[string]any without issues
 
 			diffProtoWithFailTest(t, is, tt.want, got)
@@ -116,6 +117,81 @@ var result_nobuild = &porterv1.PorterApp{
 	},
 }
 
+var v1_result_nobuild_no_image = &porterv1.PorterApp{
+	Name: "test-app",
+	Services: map[string]*porterv1.Service{
+		"example-job": {
+			Run:          "echo 'hello world'",
+			CpuCores:     0.1,
+			RamMegabytes: 256,
+			Config: &porterv1.Service_JobConfig{
+				JobConfig: &porterv1.JobServiceConfig{
+					AllowConcurrent: true,
+					Cron:            "*/10 * * * *",
+				},
+			},
+			Type: 3,
+		},
+		"example-wkr": {
+			Run:          "echo 'work'",
+			Instances:    1,
+			Port:         80,
+			CpuCores:     0.1,
+			RamMegabytes: 256,
+			Config: &porterv1.Service_WorkerConfig{
+				WorkerConfig: &porterv1.WorkerServiceConfig{
+					Autoscaling: nil,
+				},
+			},
+			Type: 2,
+		},
+		"example-web": {
+			Run:          "node index.js",
+			Instances:    0,
+			Port:         8080,
+			CpuCores:     0.1,
+			RamMegabytes: 256,
+			Config: &porterv1.Service_WebConfig{
+				WebConfig: &porterv1.WebServiceConfig{
+					Autoscaling: &porterv1.Autoscaling{
+						Enabled:                true,
+						MinInstances:           1,
+						MaxInstances:           3,
+						CpuThresholdPercent:    60,
+						MemoryThresholdPercent: 60,
+					},
+					Domains: []*porterv1.Domain{
+						{
+							Name: "test1.example.com",
+						},
+						{
+							Name: "test2.example.com",
+						},
+					},
+					HealthCheck: &porterv1.HealthCheck{
+						Enabled:  true,
+						HttpPath: "/healthz",
+					},
+				},
+			},
+			Type: 1,
+		},
+	},
+	Env: map[string]string{
+		"PORT":     "8080",
+		"NODE_ENV": "production",
+	},
+	Predeploy: &porterv1.Service{
+		Run:          "ls",
+		Instances:    0,
+		Port:         0,
+		CpuCores:     0,
+		RamMegabytes: 0,
+		Config:       &porterv1.Service_JobConfig{},
+		Type:         3,
+	},
+}
+
 func diffProtoWithFailTest(t *testing.T, is *is.I, want, got *porterv1.PorterApp) {
 	t.Helper()
 

+ 3 - 0
internal/porter_app/revisions.go

@@ -12,6 +12,8 @@ import (
 
 // Revision represents the data for a single revision
 type Revision struct {
+	// ID is the revision id
+	ID string `json:"id"`
 	// B64AppProto is the base64 encoded app proto definition
 	B64AppProto string `json:"b64_app_proto"`
 	// Status is the status of the revision
@@ -50,6 +52,7 @@ func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevi
 	revision = Revision{
 		B64AppProto:    b64,
 		Status:         appRevision.Status,
+		ID:             appRevision.Id,
 		RevisionNumber: appRevision.RevisionNumber,
 		CreatedAt:      appRevision.CreatedAt.AsTime(),
 		UpdatedAt:      appRevision.UpdatedAt.AsTime(),

+ 95 - 0
internal/porter_app/testdata/v1_input_no_build_no_image.yaml

@@ -0,0 +1,95 @@
+version: v1stack
+apps:
+  example-job:
+    type: job
+    run: echo 'hello world'
+    config:
+      allowConcurrent: true
+      resources:
+        requests:
+          cpu: 100m
+          memory: 256Mi
+      schedule:
+        enabled: true
+        value: '*/10 * * * *'
+      paused: true
+      cloudsql:
+        enabled: false
+        connectionName: ''
+        dbPort: '5432'
+        serviceAccountJSON: ''
+  example-wkr:
+    type: worker
+    run: "echo 'work'"
+    config:
+      replicaCount: '1'
+      container:
+        port: '80'
+      resources:
+        requests:
+          cpu: 100m
+          memory: 256Mi
+      autoscaling:
+        enabled: false
+        minReplicas: '1'
+        maxReplicas: '10'
+        targetCPUUtilizationPercentage: '50'
+        targetMemoryUtilizationPercentage: '50'
+      cloudsql:
+        enabled: false
+        connectionName: ''
+        dbPort: '5432'
+        serviceAccountJSON: ''
+  example-web:
+    type: web
+    run: node index.js
+    config:
+      replicaCount: '0'
+      resources:
+        requests:
+          cpu: 100m
+          memory: 256Mi
+      container:
+        port: '8080'
+      autoscaling:
+        enabled: true
+        minReplicas: '1'
+        maxReplicas: '3'
+        targetCPUUtilizationPercentage: '60'
+        targetMemoryUtilizationPercentage: '60'
+      ingress:
+        enabled: true
+        custom_domain: true
+        hosts:
+          - test1.example.com
+          - test2.example.com
+        porter_hosts: []
+        annotations:
+      service:
+        port: '8080'
+      health:
+        startupProbe:
+          enabled: false
+          failureThreshold: '3'
+          path: /startupz
+          periodSeconds: '5'
+        readinessProbe:
+          enabled: true
+          failureThreshold: '3'
+          path: /healthz
+          initialDelaySeconds: '0'
+        livenessProbe:
+          enabled: true
+          failureThreshold: '3'
+          path: /healthz
+          periodSeconds: '5'
+      cloudsql:
+        enabled: false
+        connectionName: ''
+        dbPort: '5432'
+        serviceAccountJSON: ''
+release:
+  run: ls
+env:
+  PORT: '8080'
+  NODE_ENV: 'production'

+ 117 - 0
internal/porter_app/v1/types.go

@@ -0,0 +1,117 @@
+package v1
+
+// ServiceConfig contains the configuration exposed to users in v1stack porter.yaml
+type ServiceConfig struct {
+	Autoscaling      *Autoscaling      `yaml:"autoscaling,omitempty" validate:"excluded_if=Type job"`
+	Container        Container         `yaml:"container"`
+	Health           *Health           `yaml:"health,omitempty" validate:"excluded_unless=Type web"`
+	Ingress          *Ingress          `yaml:"ingress"`
+	ReplicaCount     string            `yaml:"replicaCount"`
+	Resources        Resources         `yaml:"resources"`
+	Service          KubernetesService `yaml:"service"`
+	AllowConcurrency bool              `yaml:"allowConcurrent" validate:"excluded_unless=Type job"`
+	Schedule         Schedule          `yaml:"schedule" validate:"excluded_unless=Type job"`
+}
+
+// Schedule contains all configuration for job schedules
+type Schedule struct {
+	Enabled bool `yaml:"enabled"`
+	// Value is the cron schedule
+	Value string `yaml:"value,omitempty"`
+}
+
+// Autoscaling contains all configuration for autoscaling in a web/worker chart
+type Autoscaling struct {
+	Enabled                           bool   `yaml:"enabled"`
+	MaxReplicas                       string `yaml:"maxReplicas"`
+	MinReplicas                       string `yaml:"minReplicas"`
+	TargetCPUUtilizationPercentage    string `yaml:"targetCPUUtilizationPercentage"`
+	TargetMemoryUtilizationPercentage string `yaml:"targetMemoryUtilizationPercentage"`
+}
+
+// Container contains all configuration for containers
+type Container struct {
+	Port string `yaml:"port"`
+}
+
+// Health contains user-configurable health probes
+type Health struct {
+	// LivenessProbe checks whether a container should be considered healthy
+	LivenessProbe LivenessProbe `yaml:"livenessProbe"`
+	// ReadinessProbe checks whether a container should be considered ready to receive traffic
+	ReadinessProbe ReadinessProbe `yaml:"readinessProbe"`
+}
+
+// LivenessProbe contains user-configurable values for a liveness probe
+type LivenessProbe struct {
+	Enabled bool `yaml:"enabled"`
+	// Path is the endpoint path to use for the probe
+	Path string `yaml:"path"`
+}
+
+// ReadinessProbe contains user-configurable values for a readiness probe
+type ReadinessProbe struct {
+	Enabled bool `yaml:"enabled"`
+	// Path is the endpoint path to use for the probe
+	Path string `yaml:"path"`
+}
+
+// Ingress contains configuration for ingress used by web charts
+type Ingress struct {
+	// Enabled specifies whether or not to use an ingress
+	Enabled bool `yaml:"enabled"`
+	// Hosts specifies the domains to include in the routing rules
+	Hosts []string `yaml:"hosts"`
+	// PorterHosts specifies the porter domains to include in the routing rules
+	PorterHosts []string `yaml:"porter_hosts"`
+	// Annotations specifies annotations to add to the ingress
+	Annotations map[string]string `yaml:"annotations"`
+}
+
+// Resources is a wrapper over requests
+type Resources struct {
+	// Requests contains configuration for resource requests
+	Requests Requests `yaml:"requests"`
+}
+
+// Requests contains configuration for resource requests
+type Requests struct {
+	// Cpu is the cpu request (e.g. 100m - m for millicores)
+	Cpu string `yaml:"cpu"`
+	// Memory is the memory request (e.g. 100Mi - Mi for mebibytes)
+	Memory string `yaml:"memory"`
+}
+
+// KubernetesService contains configuration for exposing services
+type KubernetesService struct {
+	// Port is the port to expose the service on. This port should match the port in the container.
+	Port string `yaml:"port"`
+}
+
+// PorterYAML represents the accepted top-level fields in a porter.yaml
+type PorterYAML struct {
+	Version  *string            `yaml:"version"`
+	Build    *Build             `yaml:"build"`
+	Env      map[string]string  `yaml:"env"`
+	Apps     map[string]Service `yaml:"apps" validate:"required_without=Services"`
+	Services map[string]Service `yaml:"services" validate:"required_without=Apps"`
+
+	Release *Service `yaml:"release"`
+}
+
+// Build represents the build settings for a Porter app
+type Build struct {
+	Context    string   `yaml:"context" validate:"dir"`
+	Method     string   `yaml:"method" validate:"required,oneof=pack docker registry"`
+	Builder    string   `yaml:"builder" validate:"required_if=Method pack"`
+	Buildpacks []string `yaml:"buildpacks"`
+	Dockerfile string   `yaml:"dockerfile" validate:"required_if=Method docker"`
+	Image      string   `yaml:"image" validate:"required_if=Method registry"`
+}
+
+// Service represents a service in a Porter app
+type Service struct {
+	Run    string        `yaml:"run"`
+	Config ServiceConfig `yaml:"config"`
+	Type   string        `yaml:"type" validate:"required, oneof=web worker job"`
+}

+ 369 - 0
internal/porter_app/v1/yaml.go

@@ -0,0 +1,369 @@
+package v1
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"math"
+	"strconv"
+	"strings"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/porter/internal/telemetry"
+
+	"gopkg.in/yaml.v2"
+)
+
+// AppProtoFromYaml converts an old version Porter YAML file into a PorterApp proto object
+func AppProtoFromYaml(ctx context.Context, porterYamlBytes []byte, appName string) (*porterv1.PorterApp, error) {
+	ctx, span := telemetry.NewSpan(ctx, "v1-app-proto-from-yaml")
+	defer span.End()
+
+	if appName == "" {
+		return nil, telemetry.Error(ctx, span, nil, "app name is empty")
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName})
+
+	if porterYamlBytes == nil {
+		return nil, telemetry.Error(ctx, span, nil, "porter yaml is nil")
+	}
+
+	porterYaml := &PorterYAML{}
+	err := yaml.Unmarshal(porterYamlBytes, porterYaml)
+	if err != nil {
+		return nil, telemetry.Error(ctx, span, err, "error unmarshaling porter yaml")
+	}
+
+	appProto := &porterv1.PorterApp{
+		Name: appName,
+		Env:  porterYaml.Env,
+	}
+
+	if porterYaml.Build != nil {
+		appProto.Build = &porterv1.Build{
+			Context:    porterYaml.Build.Context,
+			Method:     porterYaml.Build.Method,
+			Builder:    porterYaml.Build.Builder,
+			Buildpacks: porterYaml.Build.Buildpacks,
+			Dockerfile: porterYaml.Build.Dockerfile,
+		}
+	}
+
+	if porterYaml.Build != nil && porterYaml.Build.Image != "" {
+		imageSpl := strings.Split(porterYaml.Build.Image, ":")
+		if len(imageSpl) == 2 {
+			appProto.Image = &porterv1.AppImage{
+				Repository: imageSpl[0],
+				Tag:        imageSpl[1],
+			}
+		} else {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "image", Value: porterYaml.Build.Image})
+			return nil, telemetry.Error(ctx, span, err, "error parsing image")
+		}
+	}
+
+	if porterYaml.Apps != nil && porterYaml.Services != nil {
+		return nil, telemetry.Error(ctx, span, nil, "'apps' and 'services' are synonymous but both were defined")
+	}
+	var services map[string]Service
+	if porterYaml.Apps != nil {
+		services = porterYaml.Apps
+	}
+
+	if porterYaml.Services != nil {
+		services = porterYaml.Services
+	}
+
+	if services == nil {
+		return nil, telemetry.Error(ctx, span, nil, "porter yaml is missing services")
+	}
+
+	serviceProtoMap := make(map[string]*porterv1.Service, 0)
+	for name, service := range services {
+		serviceType, err := protoEnumFromType(name, service)
+		if err != nil {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "failing-service-name", Value: name})
+			return nil, telemetry.Error(ctx, span, err, "error getting service type")
+		}
+
+		serviceProto, err := serviceProtoFromConfig(service, serviceType)
+		if err != nil {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "failing-service-name", Value: name})
+			return nil, telemetry.Error(ctx, span, err, "error casting service config")
+		}
+
+		serviceProtoMap[name] = serviceProto
+	}
+	appProto.Services = serviceProtoMap
+
+	if porterYaml.Release != nil {
+		predeployProto, err := serviceProtoFromConfig(*porterYaml.Release, porterv1.ServiceType_SERVICE_TYPE_JOB)
+		if err != nil {
+			return nil, telemetry.Error(ctx, span, err, "error casting predeploy config")
+		}
+		appProto.Predeploy = predeployProto
+	}
+
+	return appProto, nil
+}
+
+func protoEnumFromType(name string, service Service) (porterv1.ServiceType, error) {
+	var serviceType porterv1.ServiceType
+
+	if service.Type != "" {
+		if service.Type == "web" {
+			return porterv1.ServiceType_SERVICE_TYPE_WEB, nil
+		}
+		if service.Type == "worker" {
+			return porterv1.ServiceType_SERVICE_TYPE_WORKER, nil
+		}
+		if service.Type == "job" {
+			return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
+		}
+
+		return serviceType, fmt.Errorf("invalid service type '%s'", service.Type)
+	}
+
+	if strings.Contains(name, "web") {
+		return porterv1.ServiceType_SERVICE_TYPE_WEB, nil
+	}
+
+	if strings.Contains(name, "wkr") {
+		return porterv1.ServiceType_SERVICE_TYPE_WORKER, nil
+	}
+
+	if strings.Contains(name, "job") {
+		return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
+	}
+
+	if name == "release" {
+		return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
+	}
+
+	return serviceType, errors.New("no type provided and could not parse service type from name")
+}
+
+func serviceProtoFromConfig(service Service, serviceType porterv1.ServiceType) (*porterv1.Service, error) {
+	serviceProto := &porterv1.Service{
+		Run:  service.Run,
+		Type: serviceType,
+	}
+
+	// if the revision number cannot be converted, it will default to 0
+	replicaCount, _ := strconv.Atoi(service.Config.ReplicaCount)
+	if replicaCount < math.MinInt32 || replicaCount > math.MaxInt32 {
+		return nil, fmt.Errorf("replica count is out of range of int32")
+	}
+	// nolint:gosec
+	serviceProto.Instances = int32(replicaCount)
+
+	if service.Config.Resources.Requests.Cpu != "" {
+		cpuCoresStr := service.Config.Resources.Requests.Cpu
+		if !strings.HasSuffix(cpuCoresStr, "m") {
+			return nil, fmt.Errorf("cpu is not in millicores")
+		}
+
+		cpuCoresStr = strings.TrimSuffix(cpuCoresStr, "m")
+		cpuCoresFloat64, err := strconv.ParseFloat(cpuCoresStr, 32)
+		if err != nil {
+			return nil, fmt.Errorf("cpu is not a float")
+		}
+		serviceProto.CpuCores = float32(cpuCoresFloat64) / 1000
+	}
+
+	if service.Config.Resources.Requests.Memory != "" {
+		memoryStr := service.Config.Resources.Requests.Memory
+		if !strings.HasSuffix(memoryStr, "Mi") {
+			return nil, fmt.Errorf("memory is not in Mi")
+		}
+
+		memoryStr = strings.TrimSuffix(memoryStr, "Mi")
+		memoryFloat64, err := strconv.ParseFloat(memoryStr, 32)
+		if err != nil {
+			return nil, fmt.Errorf("memory is not a float")
+		}
+		// nolint:gosec
+		serviceProto.RamMegabytes = int32(memoryFloat64)
+	}
+
+	if service.Config.Container.Port != "" && service.Config.Service.Port != "" && service.Config.Container.Port != service.Config.Service.Port {
+		return nil, errors.New("container port and service port do not match")
+	}
+	if service.Config.Container.Port != "" {
+		port, err := strconv.Atoi(service.Config.Container.Port)
+		if err != nil {
+			return nil, fmt.Errorf("container port cannot be converted to int: %w", err)
+		}
+		if port < math.MinInt32 || port > math.MaxInt32 {
+			return nil, fmt.Errorf("port is out of range of int32")
+		}
+		// nolint:gosec
+		serviceProto.Port = int32(port)
+	}
+	if service.Config.Service.Port != "" {
+		port, err := strconv.Atoi(service.Config.Service.Port)
+		if err != nil {
+			return nil, fmt.Errorf("service port cannot be converted to int: %w", err)
+		}
+		if port < math.MinInt32 || port > math.MaxInt32 {
+			return nil, fmt.Errorf("port is out of range of int32")
+		}
+		// nolint:gosec
+		serviceProto.Port = int32(port)
+	}
+
+	switch serviceType {
+	default:
+		return nil, fmt.Errorf("invalid service type '%s'", serviceType)
+	case porterv1.ServiceType_SERVICE_TYPE_UNSPECIFIED:
+		return nil, errors.New("KubernetesService type unspecified")
+	case porterv1.ServiceType_SERVICE_TYPE_WEB:
+		webConfig, err := webConfigProtoFromConfig(service)
+		if err != nil {
+			return nil, fmt.Errorf("error converting web config: %w", err)
+		}
+
+		serviceProto.Config = &porterv1.Service_WebConfig{
+			WebConfig: webConfig,
+		}
+	case porterv1.ServiceType_SERVICE_TYPE_WORKER:
+		workerConfig, err := workerConfigProtoFromConfig(service)
+		if err != nil {
+			return nil, fmt.Errorf("error converting worker config: %w", err)
+		}
+
+		serviceProto.Config = &porterv1.Service_WorkerConfig{
+			WorkerConfig: workerConfig,
+		}
+	case porterv1.ServiceType_SERVICE_TYPE_JOB:
+		jobConfig := &porterv1.JobServiceConfig{
+			AllowConcurrent: service.Config.AllowConcurrency,
+			Cron:            service.Config.Schedule.Value,
+		}
+
+		serviceProto.Config = &porterv1.Service_JobConfig{
+			JobConfig: jobConfig,
+		}
+	}
+
+	return serviceProto, nil
+}
+
+func workerConfigProtoFromConfig(service Service) (*porterv1.WorkerServiceConfig, error) {
+	workerConfig := &porterv1.WorkerServiceConfig{}
+
+	var autoscaling *porterv1.Autoscaling
+	if service.Config.Autoscaling != nil && service.Config.Autoscaling.Enabled {
+		autoscaling = &porterv1.Autoscaling{
+			Enabled: service.Config.Autoscaling.Enabled,
+		}
+		minReplicas, _ := strconv.Atoi(service.Config.Autoscaling.MinReplicas)
+		if minReplicas < math.MinInt32 || minReplicas > math.MaxInt32 {
+			return nil, fmt.Errorf("minReplicas is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MinInstances = int32(minReplicas)
+		maxReplicas, _ := strconv.Atoi(service.Config.Autoscaling.MaxReplicas)
+		if maxReplicas < math.MinInt32 || maxReplicas > math.MaxInt32 {
+			return nil, fmt.Errorf("maxReplicas is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MaxInstances = int32(maxReplicas)
+		cpuThresholdPercent, _ := strconv.Atoi(service.Config.Autoscaling.TargetCPUUtilizationPercentage)
+		if cpuThresholdPercent < math.MinInt32 || cpuThresholdPercent > math.MaxInt32 {
+			return nil, fmt.Errorf("cpuThresholdPercent is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.CpuThresholdPercent = int32(cpuThresholdPercent)
+		memoryThresholdPercent, _ := strconv.Atoi(service.Config.Autoscaling.TargetMemoryUtilizationPercentage)
+		if memoryThresholdPercent < math.MinInt32 || memoryThresholdPercent > math.MaxInt32 {
+			return nil, fmt.Errorf("memoryThresholdPercent is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MemoryThresholdPercent = int32(memoryThresholdPercent)
+	}
+	workerConfig.Autoscaling = autoscaling
+
+	return workerConfig, nil
+}
+
+func webConfigProtoFromConfig(service Service) (*porterv1.WebServiceConfig, error) {
+	webConfig := &porterv1.WebServiceConfig{}
+
+	var autoscaling *porterv1.Autoscaling
+	if service.Config.Autoscaling != nil && service.Config.Autoscaling.Enabled {
+		autoscaling = &porterv1.Autoscaling{
+			Enabled: service.Config.Autoscaling.Enabled,
+		}
+		minReplicas, _ := strconv.Atoi(service.Config.Autoscaling.MinReplicas)
+		if minReplicas < math.MinInt32 || minReplicas > math.MaxInt32 {
+			return nil, errors.New("minReplicas is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MinInstances = int32(minReplicas)
+		maxReplicas, _ := strconv.Atoi(service.Config.Autoscaling.MaxReplicas)
+		if maxReplicas < math.MinInt32 || maxReplicas > math.MaxInt32 {
+			return nil, errors.New("maxReplicas is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MaxInstances = int32(maxReplicas)
+		cpuThresholdPercent, _ := strconv.Atoi(service.Config.Autoscaling.TargetCPUUtilizationPercentage)
+		if cpuThresholdPercent < math.MinInt32 || cpuThresholdPercent > math.MaxInt32 {
+			return nil, fmt.Errorf("cpuThresholdPercent is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.CpuThresholdPercent = int32(cpuThresholdPercent)
+		memoryThresholdPercent, _ := strconv.Atoi(service.Config.Autoscaling.TargetMemoryUtilizationPercentage)
+		if memoryThresholdPercent < math.MinInt32 || memoryThresholdPercent > math.MaxInt32 {
+			return nil, fmt.Errorf("memoryThresholdPercent is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MemoryThresholdPercent = int32(memoryThresholdPercent)
+	}
+	webConfig.Autoscaling = autoscaling
+
+	var healthCheck *porterv1.HealthCheck
+	// note that we are only reading from the readiness probe config, since readiness and liveness share the same config now
+	if service.Config.Health != nil {
+		health := service.Config.Health
+		if health.ReadinessProbe.Enabled && health.LivenessProbe.Enabled && health.ReadinessProbe.Path != health.LivenessProbe.Path {
+			return nil, errors.New("liveness and readiness probes must have the same path")
+		}
+		if health.ReadinessProbe.Enabled {
+			healthCheck = &porterv1.HealthCheck{
+				Enabled:  service.Config.Health.ReadinessProbe.Enabled,
+				HttpPath: service.Config.Health.ReadinessProbe.Path,
+			}
+		} else if health.LivenessProbe.Enabled {
+			healthCheck = &porterv1.HealthCheck{
+				Enabled:  service.Config.Health.LivenessProbe.Enabled,
+				HttpPath: service.Config.Health.LivenessProbe.Path,
+			}
+		}
+	}
+
+	webConfig.HealthCheck = healthCheck
+
+	if service.Config.Ingress != nil {
+		domains := make([]*porterv1.Domain, 0)
+		for _, domain := range service.Config.Ingress.Hosts {
+			hostName := domain
+			domains = append(domains, &porterv1.Domain{
+				Name: hostName,
+			})
+		}
+		for _, domain := range service.Config.Ingress.PorterHosts {
+			hostName := domain
+			domains = append(domains, &porterv1.Domain{
+				Name: hostName,
+			})
+		}
+		if service.Config.Ingress.Annotations != nil && len(service.Config.Ingress.Annotations) > 0 {
+			return nil, errors.New("annotations are not supported")
+		}
+		webConfig.Domains = domains
+		webConfig.Private = !service.Config.Ingress.Enabled
+	}
+
+	return webConfig, nil
+}

+ 5 - 0
zarf/helm/.serverenv

@@ -31,6 +31,11 @@ GITHUB_APP_ID=<github_app_id>
 
 GITHUB_APP_SECRET_PATH=<path_to_secret>
 
+# LAUNCHDARKLY_SDK_KEY is used to interact with the launchdarkly api
+# Retrieve your SDK key from https://app.launchdarkly.com/settings/projects/dev/environments
+
+LAUNCHDARKLY_SDK_KEY=<launchdarkly_sdk_key>
+
 # Optional parameters
 
 # HELM_APP_REPO_URL=http://chartmuseum:8080 can be used to test charts using porter-charts with Tilt