Bläddra i källkod

Merge pull request #2316 from porter-dev/staging

GAR, custom form component, preview env improvements -> production
abelanger5 3 år sedan
förälder
incheckning
0f1adce431
85 ändrade filer med 2498 tillägg och 340 borttagningar
  1. 7 0
      .github/workflows/prerelease.yaml
  2. 3 2
      .gitignore
  3. 1 1
      Makefile
  4. 20 0
      api/client/registry.go
  5. 45 20
      api/server/handlers/environment/create.go
  6. 45 28
      api/server/handlers/environment/create_deployment.go
  7. 13 8
      api/server/handlers/environment/delete.go
  8. 27 22
      api/server/handlers/environment/delete_deployment.go
  9. 14 7
      api/server/handlers/environment/enable_pull_request.go
  10. 1 1
      api/server/handlers/environment/finalize_deployment.go
  11. 4 9
      api/server/handlers/environment/finalize_deployment_with_errors.go
  12. 8 2
      api/server/handlers/environment/list.go
  13. 1 1
      api/server/handlers/environment/update_deployment.go
  14. 2 0
      api/server/handlers/infra/create.go
  15. 105 18
      api/server/handlers/infra/forms.go
  16. 2 0
      api/server/handlers/infra/get_template.go
  17. 8 0
      api/server/handlers/infra/list_templates.go
  18. 1 1
      api/server/handlers/registry/create.go
  19. 63 0
      api/server/handlers/registry/get_token.go
  20. 1 0
      api/server/handlers/stack/create.go
  21. 64 0
      api/server/handlers/stack/update_stack.go
  22. 1 1
      api/server/router/git_installation.go
  23. 28 0
      api/server/router/project.go
  24. 57 1
      api/server/router/v1/stack.go
  25. 1 0
      api/types/infra.go
  26. 5 0
      api/types/integrations.go
  27. 7 2
      api/types/registry.go
  28. 12 1
      api/types/stacks.go
  29. 63 46
      cli/cmd/apply.go
  30. 26 0
      cli/cmd/connect.go
  31. 92 0
      cli/cmd/connect/gar.go
  32. 2 2
      cli/cmd/connect/gcr.go
  33. 66 3
      cli/cmd/deploy.go
  34. 5 0
      cli/cmd/deploy/create.go
  35. 50 0
      cli/cmd/docker/auth.go
  36. 2 2
      cli/cmd/preview/build_image_driver.go
  37. 1 1
      cli/cmd/preview/env_group_driver.go
  38. 1 1
      cli/cmd/preview/push_image_driver.go
  39. 2 2
      cli/cmd/preview/update_config_driver.go
  40. 17 14
      cli/cmd/preview/utils.go
  41. 4 3
      cmd/app/main.go
  42. 28 0
      cmd/migrate/main.go
  43. 365 0
      cmd/migrate/populate_source_config_display_name/helpers_test.go
  44. 40 0
      cmd/migrate/populate_source_config_display_name/populate.go
  45. 76 0
      cmd/migrate/populate_source_config_display_name/populate_test.go
  46. 8 0
      dashboard/src/components/porter-form/FormDebugger.tsx
  47. 4 0
      dashboard/src/components/porter-form/PorterForm.tsx
  48. 94 0
      dashboard/src/components/porter-form/field-components/UrlLink.tsx
  49. 11 2
      dashboard/src/components/porter-form/types.ts
  50. 3 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  51. 3 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  52. 5 1
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx
  53. 114 73
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx
  54. 67 5
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx
  55. 20 2
      dashboard/src/main/home/cluster-dashboard/stacks/launch/SelectSource.tsx
  56. 4 1
      dashboard/src/main/home/cluster-dashboard/stacks/launch/Store.tsx
  57. 2 0
      dashboard/src/main/home/cluster-dashboard/stacks/types.ts
  58. 3 0
      dashboard/src/main/home/integrations/create-integration/CreateIntegrationForm.tsx
  59. 156 0
      dashboard/src/main/home/integrations/create-integration/GARForm.tsx
  60. 6 0
      dashboard/src/main/home/onboarding/components/ProviderSelector.tsx
  61. 26 0
      dashboard/src/main/home/onboarding/constants.ts
  62. 3 1
      dashboard/src/main/home/onboarding/state/StateHandler.ts
  63. 12 0
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx
  64. 135 0
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx
  65. 1 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx
  66. 14 35
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx
  67. 13 0
      dashboard/src/main/home/onboarding/types.ts
  68. 17 0
      dashboard/src/shared/api.tsx
  69. 10 0
      dashboard/src/shared/common.tsx
  70. 8 0
      dashboard/src/shared/types.tsx
  71. 7 3
      go.mod
  72. 78 0
      go.sum
  73. 2 0
      internal/helm/postrenderer.go
  74. 3 1
      internal/kubernetes/prometheus/metrics.go
  75. 5 1
      internal/models/registry.go
  76. 3 0
      internal/models/stack.go
  77. 232 1
      internal/registry/registry.go
  78. 8 0
      internal/repository/gorm/stack.go
  79. 1 0
      internal/repository/stack.go
  80. 4 0
      internal/repository/test/stack.go
  81. 1 0
      internal/stacks/helpers.go
  82. 1 1
      provisioner/server/handlers/provision/apply.go
  83. 17 2
      provisioner/server/handlers/state/create_resource.go
  84. 1 1
      provisioner/server/handlers/state/delete_resource.go
  85. 10 10
      workers/utils/retry_helm_agent.go

+ 7 - 0
.github/workflows/prerelease.yaml

@@ -577,6 +577,13 @@ jobs:
           git config user.email "support@porter.run"
           git config user.email "support@porter.run"
 
 
           git diff --quiet --exit-code || git add . && git commit -m "Update to Porter GHA version ${{steps.tag_name.outputs.tag}}" && git push -f
           git diff --quiet --exit-code || git add . && git commit -m "Update to Porter GHA version ${{steps.tag_name.outputs.tag}}" && git push -f
+
+          git checkout test-preview-env
+          git merge main -m "Merge with main"
+
+          sed -i 's/TEST:v.*/TEST:${{ steps.tag_name.outputs.tag }}/g' porter.yaml
+
+          git diff --quiet --exit-code || git add . && git commit -m "Update to Porter GHA version ${{steps.tag_name.outputs.tag}}" && git push -f
   run-new-release-tests-workflows:
   run-new-release-tests-workflows:
     name: Run new-release-tests Porter workflows
     name: Run new-release-tests Porter workflows
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest

+ 3 - 2
.gitignore

@@ -15,6 +15,7 @@ staging.sh
 *.key
 *.key
 bin
 bin
 openapi.yaml
 openapi.yaml
+.idea
 
 
 # Local docs directories
 # Local docs directories
 /docs/.obsidian
 /docs/.obsidian
@@ -37,8 +38,8 @@ openapi.yaml
 crash.log
 crash.log
 
 
 # Exclude all .tfvars files, which are likely to contain sentitive data, such as
 # Exclude all .tfvars files, which are likely to contain sentitive data, such as
-# password, private keys, and other secrets. These should not be part of version 
-# control as they are data points which are potentially sensitive and subject 
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
 # to change depending on the environment.
 # to change depending on the environment.
 #
 #
 *.tfvars
 *.tfvars

+ 1 - 1
Makefile

@@ -17,7 +17,7 @@ build-cli:
 	go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd/config.Version=${VERSION}'" -a -tags cli -o $(BINDIR)/porter ./cli
 	go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd/config.Version=${VERSION}'" -a -tags cli -o $(BINDIR)/porter ./cli
 
 
 build-cli-dev:
 build-cli-dev:
-	go build -tags cli -o $(BINDIR)/porter ./cli
+	go build -ldflags="-X 'github.com/porter-dev/porter/cli/cmd/config.Version=${VERSION}'" -tags cli -o $(BINDIR)/porter ./cli
 
 
 start-provisioner-dev: install setup-env-files
 start-provisioner-dev: install setup-env-files
 	bash ./scripts/dev-environment/StartProvisionerServer.sh
 	bash ./scripts/dev-environment/StartProvisionerServer.sh

+ 20 - 0
api/client/registry.go

@@ -123,6 +123,26 @@ func (c *Client) GetGCRAuthorizationToken(
 	return resp, err
 	return resp, err
 }
 }
 
 
+// GetGARAuthorizationToken gets a GAR authorization token
+func (c *Client) GetGARAuthorizationToken(
+	ctx context.Context,
+	projectID uint,
+	req *types.GetRegistryGARTokenRequest,
+) (*types.GetRegistryTokenResponse, error) {
+	resp := &types.GetRegistryTokenResponse{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/registries/gar/token",
+			projectID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
 // GetACRAuthorizationToken gets a ACR authorization token
 // GetACRAuthorizationToken gets a ACR authorization token
 func (c *Client) GetACRAuthorizationToken(
 func (c *Client) GetACRAuthorizationToken(
 	ctx context.Context,
 	ctx context.Context,

+ 45 - 20
api/server/handlers/environment/create.go

@@ -1,6 +1,7 @@
 package environment
 package environment
 
 
 import (
 import (
+	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
@@ -87,7 +88,7 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 
 	// create incoming webhook
 	// create incoming webhook
 	hook, _, err := client.Repositories.CreateHook(
 	hook, _, err := client.Repositories.CreateHook(
-		r.Context(), owner, name, &github.Hook{
+		context.Background(), owner, name, &github.Hook{
 			Config: map[string]interface{}{
 			Config: map[string]interface{}{
 				"url":          webhookURL,
 				"url":          webhookURL,
 				"content_type": "json",
 				"content_type": "json",
@@ -98,10 +99,9 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		},
 		},
 	)
 	)
 
 
-	if err != nil && !strings.Contains(err.Error(), "already exists on this repository") {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error trying to create a new github repository webhook: %w", err), http.StatusConflict),
-		)
+	if err != nil && !strings.Contains(err.Error(), "already exists") {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
+			http.StatusConflict))
 		return
 		return
 	}
 	}
 
 
@@ -110,6 +110,14 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	env, err = c.Repo().Environment().CreateEnvironment(env)
 	env, err = c.Repo().Environment().CreateEnvironment(env)
 
 
 	if err != nil {
 	if err != nil {
+		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
+				http.StatusConflict, "error creating environment"))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
@@ -118,14 +126,44 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	jwt, err := token.GetTokenForAPI(user.ID, project.ID)
 	jwt, err := token.GetTokenForAPI(user.ID, project.ID)
 
 
 	if err != nil {
 	if err != nil {
-		c.deleteEnvAndReportError(w, r, env, err)
+		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
+				http.StatusConflict, "error getting token for API while creating environment"))
+			return
+		}
+
+		_, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(deleteErr))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
 
 
 	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
 	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
 
 
 	if err != nil {
 	if err != nil {
-		c.deleteEnvAndReportError(w, r, env, err)
+		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
+				http.StatusConflict, "error encoding token while creating environment"))
+			return
+		}
+
+		_, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
+
+		if deleteErr != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(deleteErr))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
 
 
@@ -159,19 +197,6 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	c.WriteResult(w, r, env.ToEnvironmentType())
 	c.WriteResult(w, r, env.ToEnvironmentType())
 }
 }
 
 
-func (c *CreateEnvironmentHandler) deleteEnvAndReportError(
-	w http.ResponseWriter, r *http.Request, env *models.Environment, err error,
-) {
-	_, delErr := c.Repo().Environment().DeleteEnvironment(env)
-
-	if delErr != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(delErr))
-		return
-	}
-
-	c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-}
-
 func getGithubClientFromEnvironment(config *config.Config, env *models.Environment) (*github.Client, error) {
 func getGithubClientFromEnvironment(config *config.Config, env *models.Environment) (*github.Client, error) {
 	// get the github app client
 	// get the github app client
 	ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)
 	ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)

+ 45 - 28
api/server/handlers/environment/create_deployment.go

@@ -2,6 +2,7 @@ package environment
 
 
 import (
 import (
 	"context"
 	"context"
+	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 
 
@@ -15,8 +16,11 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
 )
 )
 
 
+var errGithubAPI = errors.New("error communicating with the github API")
+
 type CreateDeploymentHandler struct {
 type CreateDeploymentHandler struct {
 	handlers.PorterHandlerReadWriter
 	handlers.PorterHandlerReadWriter
 	authz.KubernetesAgentGetter
 	authz.KubernetesAgentGetter
@@ -54,6 +58,13 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 
 	if err != nil {
 	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(
+				fmt.Errorf("error creating deployment: no environment found")),
+			)
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
@@ -70,22 +81,21 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	prClosed, err := isGithubPRClosed(client, owner, name, int(request.PullRequestID))
 	prClosed, err := isGithubPRClosed(client, owner, name, int(request.PullRequestID))
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error fetching details of github PR. Error: %w", err), http.StatusConflict,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
 		return
 		return
 	}
 	}
 
 
 	if prClosed {
 	if prClosed {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
-			http.StatusConflict))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("cannot create deployment for closed github PR"), http.StatusConflict,
+		))
 		return
 		return
 	}
 	}
 
 
-	ghDeployment, err := createDeployment(client, env, request.PRBranchFrom, request.ActionID)
+	ghDeployment, err := createGithubDeployment(client, env, request.PRBranchFrom, request.ActionID)
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
 		return
 		return
 	}
 	}
 
 
@@ -105,6 +115,20 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
+		// try to delete the GitHub deployment
+		_, err = client.Repositories.DeleteDeployment(
+			context.Background(),
+			env.GitRepoOwner,
+			env.GitRepoName,
+			ghDeployment.GetID(),
+		)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
+				http.StatusConflict))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
@@ -127,7 +151,7 @@ func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	c.WriteResult(w, r, depl.ToDeploymentType())
 	c.WriteResult(w, r, depl.ToDeploymentType())
 }
 }
 
 
-func createDeployment(
+func createGithubDeployment(
 	client *github.Client,
 	client *github.Client,
 	env *models.Environment,
 	env *models.Environment,
 	branchFrom string,
 	branchFrom string,
@@ -135,46 +159,39 @@ func createDeployment(
 ) (*github.Deployment, error) {
 ) (*github.Deployment, error) {
 	requiredContexts := []string{}
 	requiredContexts := []string{}
 
 
-	deploymentRequest := github.DeploymentRequest{
-		Ref:              github.String(branchFrom),
-		Environment:      github.String(env.Name),
-		AutoMerge:        github.Bool(false),
-		RequiredContexts: &requiredContexts,
-	}
-
 	deployment, _, err := client.Repositories.CreateDeployment(
 	deployment, _, err := client.Repositories.CreateDeployment(
 		context.Background(),
 		context.Background(),
 		env.GitRepoOwner,
 		env.GitRepoOwner,
 		env.GitRepoName,
 		env.GitRepoName,
-		&deploymentRequest,
+		&github.DeploymentRequest{
+			Ref:              github.String(branchFrom),
+			Environment:      github.String(env.Name),
+			AutoMerge:        github.Bool(false),
+			RequiredContexts: &requiredContexts,
+		},
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%v: %w", errGithubAPI, err)
 	}
 	}
 
 
 	depID := deployment.GetID()
 	depID := deployment.GetID()
 
 
 	// Create Deployment Status to indicate it's in progress
 	// Create Deployment Status to indicate it's in progress
-
-	state := "in_progress"
-	log_url := fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d", env.GitRepoOwner, env.GitRepoName, actionID)
-
-	deploymentStatusRequest := github.DeploymentStatusRequest{
-		State:  &state,
-		LogURL: &log_url, // link to actions tab
-	}
-
 	_, _, err = client.Repositories.CreateDeploymentStatus(
 	_, _, err = client.Repositories.CreateDeploymentStatus(
 		context.Background(),
 		context.Background(),
 		env.GitRepoOwner,
 		env.GitRepoOwner,
 		env.GitRepoName,
 		env.GitRepoName,
 		depID,
 		depID,
-		&deploymentStatusRequest,
+		&github.DeploymentStatusRequest{
+			State: github.String("in_progress"),
+			LogURL: github.String(fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d",
+				env.GitRepoOwner, env.GitRepoName, actionID)), // link to actions tab
+		},
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("%v: %w", errGithubAPI, err)
 	}
 	}
 
 
 	return deployment, nil
 	return deployment, nil

+ 13 - 8
api/server/handlers/environment/delete.go

@@ -17,6 +17,7 @@ import (
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
 )
 )
 
 
 type DeleteEnvironmentHandler struct {
 type DeleteEnvironmentHandler struct {
@@ -50,14 +51,11 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 	env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	// delete Github actions files from the repo
-	client, err := getGithubClientFromEnvironment(c.Config(), env)
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("environment not found in cluster and project")))
+			return
+		}
 
 
-	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
@@ -92,6 +90,13 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 		return
 	}
 	}
 
 
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	// FIXME: ignore the return status codes for now, should be fixed when we start returning all non-fatal errors
 	// FIXME: ignore the return status codes for now, should be fixed when we start returning all non-fatal errors
 	if ghWebhookID != 0 {
 	if ghWebhookID != 0 {
 		client.Repositories.DeleteHook(context.Background(), owner, name, ghWebhookID)
 		client.Repositories.DeleteHook(context.Background(), owner, name, ghWebhookID)
@@ -129,7 +134,7 @@ func (c *DeleteEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	if err != nil {
 	if err != nil {
 		if errors.Is(err, actions.ErrProtectedBranch) {
 		if errors.Is(err, actions.ErrProtectedBranch) {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("We were unable to delete the Porter Preview Environment workflow files for this "+
+				fmt.Errorf("we were unable to delete the Porter Preview Environment workflow files for this "+
 					"repository as the default branch is protected. Please manually delete them."), http.StatusConflict,
 					"repository as the default branch is protected. Please manually delete them."), http.StatusConflict,
 			))
 			))
 			return
 			return

+ 27 - 22
api/server/handlers/environment/delete_deployment.go

@@ -50,6 +50,11 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	depl, err := c.Repo().Environment().ReadDeploymentByID(project.ID, cluster.ID, deplID)
 	depl, err := c.Repo().Environment().ReadDeploymentByID(project.ID, cluster.ID, deplID)
 
 
 	if err != nil {
 	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("deployment id not found in cluster and project")))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
@@ -85,41 +90,41 @@ func (c *DeleteDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 		return
 	}
 	}
 
 
-	client, err := getGithubClientFromEnvironment(c.Config(), env)
+	depl.Status = types.DeploymentStatusInactive
+
+	// update the deployment to mark it inactive
+	depl, err = c.Repo().Environment().UpdateDeployment(depl)
 
 
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
 
 
-	// Create new deployment status to indicate deployment is ready
-	state := "inactive"
-
-	deploymentStatusRequest := github.DeploymentStatusRequest{
-		State: &state,
-	}
-
-	_, _, err = client.Repositories.CreateDeploymentStatus(
-		context.Background(),
-		env.GitRepoOwner,
-		env.GitRepoName,
-		depl.GHDeploymentID,
-		&deploymentStatusRequest,
-	)
+	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
 
 
-	depl.Status = types.DeploymentStatusInactive
-
-	// update the deployment to mark it inactive
-	depl, err = c.Repo().Environment().UpdateDeployment(depl)
+	if depl.GHDeploymentID != 0 {
+		// set the GitHub deployment status to be inactive
+		_, _, err := client.Repositories.CreateDeploymentStatus(
+			context.Background(),
+			env.GitRepoOwner,
+			env.GitRepoName,
+			depl.GHDeploymentID,
+			&github.DeploymentStatusRequest{
+				State: github.String("inactive"),
+			},
+		)
 
 
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("%v: %w", errGithubAPI, err), http.StatusConflict,
+			))
+			return
+		}
 	}
 	}
 
 
 	c.WriteResult(w, r, depl.ToDeploymentType())
 	c.WriteResult(w, r, depl.ToDeploymentType())

+ 14 - 7
api/server/handlers/environment/enable_pull_request.go

@@ -1,6 +1,7 @@
 package environment
 package environment
 
 
 import (
 import (
+	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
@@ -14,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
 )
 )
 
 
 type EnablePullRequestHandler struct {
 type EnablePullRequestHandler struct {
@@ -44,6 +46,11 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	env, err := c.Repo().Environment().ReadEnvironmentByOwnerRepoName(project.ID, cluster.ID, request.RepoOwner, request.RepoName)
 	env, err := c.Repo().Environment().ReadEnvironmentByOwnerRepoName(project.ID, cluster.ID, request.RepoOwner, request.RepoName)
 
 
 	if err != nil {
 	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("environment not found in cluster and project")))
+			return
+		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
@@ -59,12 +66,13 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	pr, _, err := client.PullRequests.Get(r.Context(), env.GitRepoOwner, env.GitRepoName, int(request.Number))
 	pr, _, err := client.PullRequests.Get(r.Context(), env.GitRepoOwner, env.GitRepoName, int(request.Number))
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
+			http.StatusConflict))
 		return
 		return
 	}
 	}
 
 
 	if pr.GetState() == "closed" {
 	if pr.GetState() == "closed" {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("cannot enable deployment for closed PR"),
 			http.StatusConflict))
 			http.StatusConflict))
 		return
 		return
 	}
 	}
@@ -86,17 +94,17 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		if ghResp.StatusCode == 404 {
 		if ghResp.StatusCode == 404 {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf(
 				fmt.Errorf(
-					"Please make sure the preview environment workflow files are present in PR branch %s and are up to"+
+					"please make sure the preview environment workflow files are present in PR branch %s and are up to"+
 						" date with the default branch", request.BranchFrom,
 						" date with the default branch", request.BranchFrom,
-				), 404),
+				), http.StatusConflict),
 			)
 			)
 			return
 			return
 		} else if ghResp.StatusCode == 422 {
 		} else if ghResp.StatusCode == 422 {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
 				fmt.Errorf(
 				fmt.Errorf(
-					"Please make sure the workflow files in PR branch %s are up to date with the default branch",
+					"please make sure the workflow files in PR branch %s are up to date with the default branch",
 					request.BranchFrom,
 					request.BranchFrom,
-				), 422),
+				), http.StatusConflict),
 			)
 			)
 			return
 			return
 		}
 		}
@@ -141,5 +149,4 @@ func (c *EnablePullRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
-
 }
 }

+ 1 - 1
api/server/handlers/environment/finalize_deployment.go

@@ -264,7 +264,7 @@ func isGithubPRClosed(
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
-		return false, err
+		return false, fmt.Errorf("%v: %w", errGithubAPI, err)
 	}
 	}
 
 
 	return ghPR.GetState() == "closed", nil
 	return ghPR.GetState() == "closed", nil

+ 4 - 9
api/server/handlers/environment/finalize_deployment_with_errors.go

@@ -61,7 +61,7 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 
 
 	if err != nil {
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no environment found")))
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("environment not found in cluster and project")))
 			return
 			return
 		}
 		}
 
 
@@ -85,9 +85,7 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("unable to get github client: %w", err), http.StatusConflict,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}
 
 
@@ -95,15 +93,12 @@ func (c *FinalizeDeploymentWithErrorsHandler) ServeHTTP(w http.ResponseWriter, r
 	prClosed, err := isGithubPRClosed(client, owner, name, int(depl.PullRequestID))
 	prClosed, err := isGithubPRClosed(client, owner, name, int(depl.PullRequestID))
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
-				depl.ID, err), http.StatusConflict,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
 		return
 		return
 	}
 	}
 
 
 	if prClosed {
 	if prClosed {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("github PR has been closed"),
 			http.StatusConflict))
 			http.StatusConflict))
 		return
 		return
 	}
 	}

+ 8 - 2
api/server/handlers/environment/list.go

@@ -1,6 +1,7 @@
 package environment
 package environment
 
 
 import (
 import (
+	"fmt"
 	"net/http"
 	"net/http"
 
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -31,7 +32,9 @@ func (c *ListEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	envs, err := c.Repo().Environment().ListEnvironments(project.ID, cluster.ID)
 	envs, err := c.Repo().Environment().ListEnvironments(project.ID, cluster.ID)
 
 
 	if err != nil {
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error listing environments"), http.StatusInternalServerError, err.Error(),
+		))
 		return
 		return
 	}
 	}
 
 
@@ -43,7 +46,10 @@ func (c *ListEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		depls, err := c.Repo().Environment().ListDeployments(env.ID)
 		depls, err := c.Repo().Environment().ListDeployments(env.ID)
 
 
 		if err != nil {
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("error listing environments: error listing deployments for environment ID %d", env.ID),
+				http.StatusInternalServerError, err.Error(),
+			))
 			return
 			return
 		}
 		}
 
 

+ 1 - 1
api/server/handlers/environment/update_deployment.go

@@ -89,7 +89,7 @@ func (c *UpdateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		return
 		return
 	}
 	}
 
 
-	ghDeployment, err := createDeployment(client, env, request.PRBranchFrom, request.ActionID)
+	ghDeployment, err := createGithubDeployment(client, env, request.PRBranchFrom, request.ActionID)
 
 
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 2 - 0
api/server/handlers/infra/create.go

@@ -207,6 +207,8 @@ func getSourceLinkAndVersion(kind types.InfraKind) (string, string) {
 		return "porter/aws/s3", "v0.1.0"
 		return "porter/aws/s3", "v0.1.0"
 	case types.InfraGCR:
 	case types.InfraGCR:
 		return "porter/gcp/gcr", "v0.1.0"
 		return "porter/gcp/gcr", "v0.1.0"
+	case types.InfraGAR:
+		return "porter/gcp/gar", "v0.1.0"
 	case types.InfraGKE:
 	case types.InfraGKE:
 		return "porter/gcp/gke", "v0.1.0"
 		return "porter/gcp/gke", "v0.1.0"
 	case types.InfraDOCR:
 	case types.InfraDOCR:

+ 105 - 18
api/server/handlers/infra/forms.go

@@ -9,7 +9,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: String to echo
       label: String to echo
     - type: string-input
     - type: string-input
@@ -27,7 +27,7 @@ tabs:
   label: Main
   label: Main
   sections:
   sections:
   - name: heading
   - name: heading
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: S3 Settings
       label: S3 Settings
   - name: bucket_name
   - name: bucket_name
@@ -48,7 +48,7 @@ tabs:
   label: Main
   label: Main
   sections:
   sections:
   - name: heading
   - name: heading
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: Database Settings
       label: Database Settings
   - name: user
   - name: user
@@ -110,7 +110,7 @@ tabs:
         - label: "Postgres 13"
         - label: "Postgres 13"
           value: postgres13
           value: postgres13
   - name: pg-9-versions
   - name: pg-9-versions
-    show_if: 
+    show_if:
       is: "postgres9"
       is: "postgres9"
       variable: db_family
       variable: db_family
     contents:
     contents:
@@ -165,7 +165,7 @@ tabs:
         - label: "v9.6.23"
         - label: "v9.6.23"
           value: "9.6.23"
           value: "9.6.23"
   - name: pg-10-versions
   - name: pg-10-versions
-    show_if: 
+    show_if:
       is: "postgres10"
       is: "postgres10"
       variable: db_family
       variable: db_family
     contents:
     contents:
@@ -212,7 +212,7 @@ tabs:
         - label: "v10.18"
         - label: "v10.18"
           value: "10.18"
           value: "10.18"
   - name: pg-11-versions
   - name: pg-11-versions
-    show_if: 
+    show_if:
       is: "postgres11"
       is: "postgres11"
       variable: db_family
       variable: db_family
     contents:
     contents:
@@ -249,7 +249,7 @@ tabs:
         - label: "v11.13"
         - label: "v11.13"
           value: "11.13"
           value: "11.13"
   - name: pg-12-versions
   - name: pg-12-versions
-    show_if: 
+    show_if:
       is: "postgres12"
       is: "postgres12"
       variable: db_family
       variable: db_family
     contents:
     contents:
@@ -276,7 +276,7 @@ tabs:
         - label: "v12.10"
         - label: "v12.10"
           value: "12.10"
           value: "12.10"
   - name: pg-13-versions
   - name: pg-13-versions
-    show_if: 
+    show_if:
       is: "postgres13"
       is: "postgres13"
       variable: db_family
       variable: db_family
     contents:
     contents:
@@ -326,7 +326,7 @@ tabs:
         default: 20
         default: 20
     - type: checkbox
     - type: checkbox
       variable: db_storage_encrypted
       variable: db_storage_encrypted
-      label: Enable storage encryption for the database. 
+      label: Enable storage encryption for the database.
       settings:
       settings:
         default: false
         default: false
 - name: advanced
 - name: advanced
@@ -353,7 +353,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: ECR Configuration
       label: ECR Configuration
     - type: string-input
     - type: string-input
@@ -371,7 +371,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: EKS Configuration
       label: EKS Configuration
     - type: select
     - type: select
@@ -513,7 +513,7 @@ tabs:
       settings:
       settings:
         default: true
         default: true
   - name: aws_auth_warning
   - name: aws_auth_warning
-    show_if: 
+    show_if:
       not: manage_aws_auth_configmap
       not: manage_aws_auth_configmap
     contents:
     contents:
     - type: subtitle
     - type: subtitle
@@ -635,7 +635,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: GCR Configuration
       label: GCR Configuration
     - type: select
     - type: select
@@ -698,6 +698,93 @@ tabs:
           value: us-west4
           value: us-west4
 `
 `
 
 
+const garForm = `name: GAR
+hasSource: false
+includeHiddenFields: true
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents:
+    - type: heading
+      label: GAR Configuration
+    - type: select
+      label: 📍 GCP Region
+      variable: gcp_region
+      settings:
+        default: us-central1
+        options:
+        - label: asia-east1
+          value: asia-east1
+        - label: asia-east2
+          value: asia-east2
+        - label: asia-northeast1
+          value: asia-northeast1
+        - label: asia-northeast2
+          value: asia-northeast2
+        - label: asia-northeast3
+          value: asia-northeast3
+        - label: asia-south1
+          value: asia-south1
+        - label: asia-south2
+          value: asia-south2
+        - label: asia-southeast1
+          value: asia-southeast1
+        - label: asia-southeast2
+          value: asia-southeast2
+        - label: australia-southeast1
+          value: australia-southeast1
+        - label: australia-southeast2
+          value: australia-southeast2
+        - label: europe-central2
+          value: europe-central2
+        - label: europe-north1
+          value: europe-north1
+        - label: europe-southwest1
+          value: europe-southwest1
+        - label: europe-west1
+          value: europe-west1
+        - label: europe-west2
+          value: europe-west2
+        - label: europe-west3
+          value: europe-west3
+        - label: europe-west4
+          value: europe-west4
+        - label: europe-west6
+          value: europe-west6
+        - label: europe-west8
+          value: europe-west8
+        - label: europe-west9
+          value: europe-west9
+        - label: northamerica-northeast1
+          value: northamerica-northeast1
+        - label: northamerica-northeast2
+          value: northamerica-northeast2
+        - label: southamerica-east1
+          value: southamerica-east1
+        - label: southamerica-west1
+          value: southamerica-west1
+        - label: us-central1
+          value: us-central1
+        - label: us-east1
+          value: us-east1
+        - label: us-east4
+          value: us-east4
+        - label: us-east5
+          value: us-east5
+        - label: us-south1
+          value: us-south1
+        - label: us-west1
+          value: us-west1
+        - label: us-west2
+          value: us-west2
+        - label: us-west3
+          value: us-west3
+        - label: us-west4
+          value: us-west4
+`
+
 const gkeForm = `name: GKE
 const gkeForm = `name: GKE
 hasSource: false
 hasSource: false
 includeHiddenFields: true
 includeHiddenFields: true
@@ -706,7 +793,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: GKE Configuration
       label: GKE Configuration
     - type: select
     - type: select
@@ -787,7 +874,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: DOCR Configuration
       label: DOCR Configuration
     - type: select
     - type: select
@@ -815,7 +902,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: DOKS Configuration
       label: DOKS Configuration
     - type: select
     - type: select
@@ -865,7 +952,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: ACR Configuration
       label: ACR Configuration
     - type: select
     - type: select
@@ -900,7 +987,7 @@ tabs:
   label: Configuration
   label: Configuration
   sections:
   sections:
   - name: section_one
   - name: section_one
-    contents: 
+    contents:
     - type: heading
     - type: heading
       label: AKS Configuration
       label: AKS Configuration
     - type: select
     - type: select

+ 2 - 0
api/server/handlers/infra/get_template.go

@@ -73,6 +73,8 @@ func getFormBytesFromKind(kind string) []byte {
 		formBytes = []byte(eksForm)
 		formBytes = []byte(eksForm)
 	case "gcr":
 	case "gcr":
 		formBytes = []byte(gcrForm)
 		formBytes = []byte(gcrForm)
+	case "gar":
+		formBytes = []byte(garForm)
 	case "gke":
 	case "gke":
 		formBytes = []byte(gkeForm)
 		formBytes = []byte(gkeForm)
 	case "docr":
 	case "docr":

+ 8 - 0
api/server/handlers/infra/list_templates.go

@@ -81,6 +81,14 @@ var templateMap = map[string]*types.InfraTemplateMeta{
 		Kind:               "gcr",
 		Kind:               "gcr",
 		RequiredCredential: "gcp_integration_id",
 		RequiredCredential: "gcp_integration_id",
 	},
 	},
+	"gar": {
+		Icon:               "https://carlossanchez.files.wordpress.com/2019/06/21046548.png?w=640",
+		Description:        "Create a Google Artifact Registry.",
+		Name:               "GAR",
+		Version:            "v0.1.0",
+		Kind:               "gar",
+		RequiredCredential: "gcp_integration_id",
+	},
 	"gke": {
 	"gke": {
 		Icon:               "https://sysdig.com/wp-content/uploads/2016/08/GKE_color.png",
 		Icon:               "https://sysdig.com/wp-content/uploads/2016/08/GKE_color.png",
 		Description:        "Create a Google Kubernetes Engine cluster.",
 		Description:        "Create a Google Kubernetes Engine cluster.",

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

@@ -83,7 +83,7 @@ func (p *RegistryCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	var err error
 	var err error
 
 
 	if request.GCPIntegrationID != 0 {
 	if request.GCPIntegrationID != 0 {
-		_, err = p.Repo().GCPIntegration().ReadGCPIntegration(proj.ID, request.GCPIntegrationID)
+		_, err := p.Repo().GCPIntegration().ReadGCPIntegration(proj.ID, request.GCPIntegrationID)
 
 
 		if err != nil {
 		if err != nil {
 			if errors.Is(err, gorm.ErrRecordNotFound) {
 			if errors.Is(err, gorm.ErrRecordNotFound) {

+ 63 - 0
api/server/handlers/registry/get_token.go

@@ -173,6 +173,69 @@ func (c *RegistryGetGCRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	c.WriteResult(w, r, resp)
 	c.WriteResult(w, r, resp)
 }
 }
 
 
+type RegistryGetGARTokenHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewRegistryGetGARTokenHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *RegistryGetGARTokenHandler {
+	return &RegistryGetGARTokenHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *RegistryGetGARTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.GetRegistryGCRTokenRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	// list registries and find one that matches the region
+	regs, err := c.Repo().Registry().ListRegistriesByProjectID(proj.ID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var token string
+	var expiresAt *time.Time
+
+	for _, reg := range regs {
+		if reg.GCPIntegrationID != 0 && strings.Contains(reg.URL, request.ServerURL) {
+			_reg := registry.Registry(*reg)
+
+			oauthTok, err := _reg.GetGARToken(c.Repo())
+
+			// if the oauth token is not nil, but the error is not nil, we still return the token
+			// but log an error
+			if oauthTok != nil && err != nil {
+				c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
+			} else if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+
+			token = oauthTok.AccessToken
+			expiresAt = &oauthTok.Expiry
+			break
+		}
+	}
+
+	resp := &types.GetRegistryTokenResponse{
+		Token:     token,
+		ExpiresAt: expiresAt,
+	}
+
+	c.WriteResult(w, r, resp)
+}
+
 type RegistryGetDOCRTokenHandler struct {
 type RegistryGetDOCRTokenHandler struct {
 	handlers.PorterHandlerReadWriter
 	handlers.PorterHandlerReadWriter
 }
 }

+ 1 - 0
api/server/handlers/stack/create.go

@@ -263,6 +263,7 @@ func getSourceConfigModels(sourceConfigs []*types.CreateStackSourceConfigRequest
 
 
 			res = append(res, models.StackSourceConfig{
 			res = append(res, models.StackSourceConfig{
 				UID:          uid,
 				UID:          uid,
+				DisplayName:  sourceConfig.DisplayName,
 				Name:         sourceConfig.Name,
 				Name:         sourceConfig.Name,
 				ImageRepoURI: sourceConfig.ImageRepoURI,
 				ImageRepoURI: sourceConfig.ImageRepoURI,
 				ImageTag:     sourceConfig.ImageTag,
 				ImageTag:     sourceConfig.ImageTag,

+ 64 - 0
api/server/handlers/stack/update_stack.go

@@ -0,0 +1,64 @@
+package stack
+
+import (
+	"fmt"
+	"net/http"
+
+	"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"
+)
+
+type StackUpdateStack struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewStackUpdateStackHandler(
+	config *config.Config,
+	reader shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *StackUpdateStack {
+	return &StackUpdateStack{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, reader, writer),
+	}
+}
+
+func (p *StackUpdateStack) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	stack, _ := r.Context().Value(types.StackScope).(*models.Stack)
+
+	req := &types.UpdateStackRequest{}
+
+	if ok := p.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	if len(stack.Revisions) == 0 {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("no stack revisions exist"), http.StatusBadRequest,
+		))
+		return
+	}
+
+	stack, err := p.Repo().Stack().ReadStackByID(proj.ID, stack.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// Update stack name
+	stack.Name = req.Name
+
+	newStack, err := p.Repo().Stack().UpdateStack(stack)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	p.WriteResult(w, r, newStack)
+}

+ 1 - 1
api/server/router/git_installation.go

@@ -115,7 +115,7 @@ func getGitInstallationRoutes(
 
 
 	if config.ServerConf.GithubIncomingWebhookSecret != "" {
 	if config.ServerConf.GithubIncomingWebhookSecret != "" {
 
 
-		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id} ->
+		// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id}/environment ->
 		// environment.NewCreateEnvironmentHandler
 		// environment.NewCreateEnvironmentHandler
 		createEnvironmentEndpoint := factory.NewAPIEndpoint(
 		createEnvironmentEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{
 			&types.APIRequestMetadata{

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

@@ -608,6 +608,34 @@ func getProjectRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	//  GET /api/projects/{project_id}/registries/gar/token -> registry.NewRegistryGetGARTokenHandler
+	getGARTokenEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/registries/gar/token",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getGARTokenHandler := registry.NewRegistryGetGARTokenHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getGARTokenEndpoint,
+		Handler:  getGARTokenHandler,
+		Router:   r,
+	})
+
 	//  GET /api/projects/{project_id}/registries/acr/token -> registry.NewRegistryGetACRTokenHandler
 	//  GET /api/projects/{project_id}/registries/acr/token -> registry.NewRegistryGetACRTokenHandler
 	getACRTokenEndpoint := factory.NewAPIEndpoint(
 	getACRTokenEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{

+ 57 - 1
api/server/router/v1/stack.go

@@ -9,7 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 )
 )
 
 
-// swagger:parameters getStack deleteStack putStackSource rollbackStack listStackRevisions addApplication addEnvGroup
+// swagger:parameters getStack deleteStack putStackSource rollbackStack listStackRevisions addApplication addEnvGroup updateStack
 type stackPathParams struct {
 type stackPathParams struct {
 	// The project id
 	// The project id
 	// in: path
 	// in: path
@@ -820,5 +820,61 @@ func getV1StackRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id} -> stack.NewStackUpdateStackHandler
+	// swagger:operation PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id} updateStack
+	//
+	// Updates a stack. Currently the only value available to update is the stack name.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Update Stack
+	// tags:
+	// - Stacks
+	// parameters:
+	//   - name: project_id
+	//   - name: cluster_id
+	//   - name: namespace
+	//   - name: stack_id
+	//   - in: body
+	//     name: UpdateStack
+	//     description: The stack to update
+	//     schema:
+	//       $ref: '#/definitions/UpdateStackRequest'
+	// responses:
+	//   '200':
+	//     description: Successfully updated the stack
+	//   '403':
+	//     description: Forbidden
+	updateStackEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPatch,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{stack_id}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+				types.StackScope,
+			},
+		},
+	)
+
+	updateStackHandler := stack.NewStackUpdateStackHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: updateStackEndpoint,
+		Handler:  updateStackHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 	return routes, newPath
 }
 }

+ 1 - 0
api/types/infra.go

@@ -23,6 +23,7 @@ const (
 	InfraECR  InfraKind = "ecr"
 	InfraECR  InfraKind = "ecr"
 	InfraEKS  InfraKind = "eks"
 	InfraEKS  InfraKind = "eks"
 	InfraGCR  InfraKind = "gcr"
 	InfraGCR  InfraKind = "gcr"
+	InfraGAR  InfraKind = "gar"
 	InfraGKE  InfraKind = "gke"
 	InfraGKE  InfraKind = "gke"
 	InfraDOCR InfraKind = "docr"
 	InfraDOCR InfraKind = "docr"
 	InfraDOKS InfraKind = "doks"
 	InfraDOKS InfraKind = "doks"

+ 5 - 0
api/types/integrations.go

@@ -48,6 +48,11 @@ var PorterRegistryIntegrations = []PorterIntegration{
 		Category:      "registry",
 		Category:      "registry",
 		Service:       string(GCR),
 		Service:       string(GCR),
 	},
 	},
+	{
+		AuthMechanism: "gcp",
+		Category:      "registry",
+		Service:       string(GAR),
+	},
 	{
 	{
 		AuthMechanism: "aws",
 		AuthMechanism: "aws",
 		Category:      "registry",
 		Category:      "registry",

+ 7 - 2
api/types/registry.go

@@ -26,7 +26,7 @@ type Registry struct {
 	URL string `json:"url"`
 	URL string `json:"url"`
 
 
 	// The integration service for this registry
 	// The integration service for this registry
-	// enum: gcr,ecr,acr,docr,dockerhub
+	// enum: gcr,gar,ecr,acr,docr,dockerhub
 	// example: ecr
 	// example: ecr
 	Service string `json:"service"`
 	Service string `json:"service"`
 
 
@@ -97,6 +97,7 @@ type RegistryService string
 
 
 const (
 const (
 	GCR       RegistryService = "gcr"
 	GCR       RegistryService = "gcr"
+	GAR       RegistryService = "gar"
 	ECR       RegistryService = "ecr"
 	ECR       RegistryService = "ecr"
 	ACR       RegistryService = "acr"
 	ACR       RegistryService = "acr"
 	DOCR      RegistryService = "docr"
 	DOCR      RegistryService = "docr"
@@ -159,7 +160,7 @@ type GetRegistryResponse Registry
 
 
 // swagger:model
 // swagger:model
 type CreateRegistryRepositoryRequest struct {
 type CreateRegistryRepositoryRequest struct {
-	// The URL to the repository of a registry (**ECR only**)
+	// The URL to the repository of a registry (ECR, GAR)
 	// required: true
 	// required: true
 	ImageRepoURI string `json:"image_repo_uri" form:"required"`
 	ImageRepoURI string `json:"image_repo_uri" form:"required"`
 }
 }
@@ -179,6 +180,10 @@ type GetRegistryGCRTokenRequest struct {
 	ServerURL string `schema:"server_url"`
 	ServerURL string `schema:"server_url"`
 }
 }
 
 
+type GetRegistryGARTokenRequest struct {
+	ServerURL string `schema:"server_url"`
+}
+
 type GetRegistryECRTokenRequest struct {
 type GetRegistryECRTokenRequest struct {
 	Region    string `schema:"region"`
 	Region    string `schema:"region"`
 	AccountID string `schema:"account_id"`
 	AccountID string `schema:"account_id"`

+ 12 - 1
api/types/stacks.go

@@ -63,6 +63,11 @@ type CreateStackAppResourceRequest struct {
 	SourceConfigName string `json:"source_config_name" form:"required"`
 	SourceConfigName string `json:"source_config_name" form:"required"`
 }
 }
 
 
+// swagger:model
+type UpdateStackRequest struct {
+	Name string `json:"name" form:"required"`
+}
+
 // swagger:model
 // swagger:model
 type Stack struct {
 type Stack struct {
 	// The time that the stack was initially created
 	// The time that the stack was initially created
@@ -207,9 +212,12 @@ type StackSourceConfig struct {
 	// The numerical revision id that this source config belongs to
 	// The numerical revision id that this source config belongs to
 	StackRevisionID uint `json:"stack_revision_id"`
 	StackRevisionID uint `json:"stack_revision_id"`
 
 
-	// The display name of the stack source
+	// Unique name for the source config
 	Name string `json:"name"`
 	Name string `json:"name"`
 
 
+	// Display name for the stack source
+	DisplayName string `json:"display_name"`
+
 	// The unique id of the stack source config
 	// The unique id of the stack source config
 	ID string `json:"id"`
 	ID string `json:"id"`
 
 
@@ -245,6 +253,9 @@ type CreateStackEnvGroupRequest struct {
 
 
 // swagger:model
 // swagger:model
 type CreateStackSourceConfigRequest struct {
 type CreateStackSourceConfigRequest struct {
+	// required: true
+	DisplayName string `json:"display_name" form:"required"`
+
 	// required: true
 	// required: true
 	Name string `json:"name" form:"required"`
 	Name string `json:"name" form:"required"`
 
 

+ 63 - 46
cli/cmd/apply.go

@@ -25,7 +25,7 @@ import (
 	"github.com/porter-dev/switchboard/pkg/models"
 	"github.com/porter-dev/switchboard/pkg/models"
 	"github.com/porter-dev/switchboard/pkg/parser"
 	"github.com/porter-dev/switchboard/pkg/parser"
 	switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
 	switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
-	"github.com/porter-dev/switchboard/pkg/worker"
+	switchboardWorker "github.com/porter-dev/switchboard/pkg/worker"
 	"github.com/rs/zerolog"
 	"github.com/rs/zerolog"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
@@ -80,25 +80,27 @@ func init() {
 	applyCmd.MarkFlagRequired("file")
 	applyCmd.MarkFlagRequired("file")
 }
 }
 
 
-func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	fileBytes, err := ioutil.ReadFile(porterYAML)
 	fileBytes, err := ioutil.ReadFile(porterYAML)
+
 	if err != nil {
 	if err != nil {
-		return err
+		return fmt.Errorf("error reading porter.yaml: %w", err)
 	}
 	}
 
 
 	resGroup, err := parser.ParseRawBytes(fileBytes)
 	resGroup, err := parser.ParseRawBytes(fileBytes)
+
 	if err != nil {
 	if err != nil {
-		return err
+		return fmt.Errorf("error parsing porter.yaml: %w", err)
 	}
 	}
 
 
 	basePath, err := os.Getwd()
 	basePath, err := os.Getwd()
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return fmt.Errorf("error getting working directory: %w", err)
 	}
 	}
 
 
-	worker := worker.NewWorker()
-	worker.RegisterDriver("deploy", NewPorterDriver)
+	worker := switchboardWorker.NewWorker()
+	worker.RegisterDriver("deploy", NewDeployDriver)
 	worker.RegisterDriver("build-image", preview.NewBuildDriver)
 	worker.RegisterDriver("build-image", preview.NewBuildDriver)
 	worker.RegisterDriver("push-image", preview.NewPushDriver)
 	worker.RegisterDriver("push-image", preview.NewPushDriver)
 	worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
 	worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
@@ -118,7 +120,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []str
 		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
 		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
 
 
 		if err != nil {
 		if err != nil {
-			return err
+			return fmt.Errorf("error creating deployment hook: %w", err)
 		}
 		}
 
 
 		worker.RegisterHook("deployment", deploymentHook)
 		worker.RegisterHook("deployment", deploymentHook)
@@ -191,7 +193,7 @@ type ApplicationConfig struct {
 	Values map[string]interface{}
 	Values map[string]interface{}
 }
 }
 
 
-type Driver struct {
+type DeployDriver struct {
 	source      *preview.Source
 	source      *preview.Source
 	target      *preview.Target
 	target      *preview.Target
 	output      map[string]interface{}
 	output      map[string]interface{}
@@ -199,21 +201,23 @@ type Driver struct {
 	logger      *zerolog.Logger
 	logger      *zerolog.Logger
 }
 }
 
 
-func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
-	driver := &Driver{
+func NewDeployDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+	driver := &DeployDriver{
 		lookupTable: opts.DriverLookupTable,
 		lookupTable: opts.DriverLookupTable,
 		logger:      opts.Logger,
 		logger:      opts.Logger,
 		output:      make(map[string]interface{}),
 		output:      make(map[string]interface{}),
 	}
 	}
 
 
-	source, err := preview.GetSource(resource.Source)
+	source, err := preview.GetSource(resource.Name, resource.Source)
+
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	driver.source = source
 	driver.source = source
 
 
-	target, err := preview.GetTarget(resource.Target)
+	target, err := preview.GetTarget(resource.Name, resource.Target)
+
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -223,16 +227,16 @@ func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts)
 	return driver, nil
 	return driver, nil
 }
 }
 
 
-func (d *Driver) ShouldApply(resource *models.Resource) bool {
+func (d *DeployDriver) ShouldApply(_ *models.Resource) bool {
 	return true
 	return true
 }
 }
 
 
-func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
+func (d *DeployDriver) Apply(resource *models.Resource) (*models.Resource, error) {
 	client := config.GetAPIClient()
 	client := config.GetAPIClient()
 	name := resource.Name
 	name := resource.Name
 
 
 	if name == "" {
 	if name == "" {
-		return nil, fmt.Errorf("empty app name")
+		return nil, fmt.Errorf("empty resource name")
 	}
 	}
 
 
 	_, err := client.GetRelease(
 	_, err := client.GetRelease(
@@ -257,11 +261,11 @@ func (d *Driver) Apply(resource *models.Resource) (*models.Resource, error) {
 }
 }
 
 
 // Simple apply for addons
 // Simple apply for addons
-func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
+func (d *DeployDriver) applyAddon(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
 	addonConfig, err := d.getAddonConfig(resource)
 	addonConfig, err := d.getAddonConfig(resource)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("error getting addon config for resource %s: %w", resource.Name, err)
 	}
 	}
 
 
 	if shouldCreate {
 	if shouldCreate {
@@ -282,13 +286,13 @@ func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shoul
 		)
 		)
 
 
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error creating addon from resource %s: %w", resource.Name, err)
 		}
 		}
 	} else {
 	} else {
 		bytes, err := json.Marshal(addonConfig)
 		bytes, err := json.Marshal(addonConfig)
 
 
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error marshalling addon config from resource %s: %w", resource.Name, err)
 		}
 		}
 
 
 		err = client.UpgradeRelease(
 		err = client.UpgradeRelease(
@@ -303,7 +307,7 @@ func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shoul
 		)
 		)
 
 
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error updating addon from resource %s: %w", resource.Name, err)
 		}
 		}
 	}
 	}
 
 
@@ -314,7 +318,11 @@ func (d *Driver) applyAddon(resource *models.Resource, client *api.Client, shoul
 	return resource, nil
 	return resource, nil
 }
 }
 
 
-func (d *Driver) applyApplication(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
+func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.Client, shouldCreate bool) (*models.Resource, error) {
+	if resource == nil {
+		return nil, fmt.Errorf("nil resource")
+	}
+
 	appConfig, err := d.getApplicationConfig(resource)
 	appConfig, err := d.getApplicationConfig(resource)
 
 
 	if err != nil {
 	if err != nil {
@@ -324,25 +332,32 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 	method := appConfig.Build.Method
 	method := appConfig.Build.Method
 
 
 	if method != "pack" && method != "docker" && method != "registry" {
 	if method != "pack" && method != "docker" && method != "registry" {
-		return nil, fmt.Errorf("method should either be \"docker\", \"pack\" or \"registry\"")
+		return nil, fmt.Errorf("for resource %s, config.build.method should either be \"docker\", \"pack\" or \"registry\"",
+			resource.Name)
 	}
 	}
 
 
 	fullPath, err := filepath.Abs(appConfig.Build.Context)
 	fullPath, err := filepath.Abs(appConfig.Build.Context)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("for resource %s, error getting absolute path for config.build.context: %w", resource.Name,
+			err)
 	}
 	}
 
 
 	tag := os.Getenv("PORTER_TAG")
 	tag := os.Getenv("PORTER_TAG")
 
 
 	if tag == "" {
 	if tag == "" {
+		color.New(color.FgYellow).Printf("for resource %s, since PORTER_TAG is not set, the Docker image tag will default to"+
+			" the git repo SHA", resource.Name)
+
 		commit, err := git.LastCommit()
 		commit, err := git.LastCommit()
 
 
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("for resource %s, error getting last git commit: %w", resource.Name, err)
 		}
 		}
 
 
 		tag = commit.Sha[:7]
 		tag = commit.Sha[:7]
+
+		color.New(color.FgYellow).Printf("for resource %s, using tag %s\n", resource.Name, tag)
 	}
 	}
 
 
 	// if the method is registry and a tag is defined, we use the provided tag
 	// if the method is registry and a tag is defined, we use the provided tag
@@ -383,16 +398,16 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 		resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
 		resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
 
 
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error creating app from resource %s: %w", resource.Name, err)
 		}
 		}
 	} else if !appConfig.OnlyCreate {
 	} else if !appConfig.OnlyCreate {
 		resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
 		resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
 
 
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error updating application from resource %s: %w", resource.Name, err)
 		}
 		}
 	} else {
 	} else {
-		color.New(color.FgYellow).Printf("Skipping creation for %s as onlyCreate is set to true\n", resource.Name)
+		color.New(color.FgYellow).Printf("Skipping creation for resource %s as onlyCreate is set to true\n", resource.Name)
 	}
 	}
 
 
 	if err = d.assignOutput(resource, client); err != nil {
 	if err = d.assignOutput(resource, client); err != nil {
@@ -430,14 +445,14 @@ func (d *Driver) applyApplication(resource *models.Resource, client *api.Client,
 	return resource, err
 	return resource, err
 }
 }
 
 
-func (d *Driver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
+func (d *DeployDriver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
 	// create new release
 	// create new release
 	color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
 	color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
 
 
 	regList, err := client.ListRegistries(context.Background(), d.target.Project)
 	regList, err := client.ListRegistries(context.Background(), d.target.Project)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("for resource %s, error listing registries: %w", resource.Name, err)
 	}
 	}
 
 
 	var registryURL string
 	var registryURL string
@@ -448,6 +463,8 @@ func (d *Driver) createApplication(resource *models.Resource, client *api.Client
 		registryURL = (*regList)[0].URL
 		registryURL = (*regList)[0].URL
 	}
 	}
 
 
+	color.New(color.FgBlue).Printf("for resource %s, using registry %s\n", resource.Name, registryURL)
+
 	// attempt to get repo suffix from environment variables
 	// attempt to get repo suffix from environment variables
 	var repoSuffix string
 	var repoSuffix string
 
 
@@ -514,7 +531,7 @@ func (d *Driver) createApplication(resource *models.Resource, client *api.Client
 	return resource, handleSubdomainCreate(subdomain, err)
 	return resource, handleSubdomainCreate(subdomain, err)
 }
 }
 
 
-func (d *Driver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
+func (d *DeployDriver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
 	color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
 	color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
 
 
 	if len(appConf.Build.Env) > 0 {
 	if len(appConf.Build.Env) > 0 {
@@ -580,7 +597,7 @@ func (d *Driver) updateApplication(resource *models.Resource, client *api.Client
 	return resource, nil
 	return resource, nil
 }
 }
 
 
-func (d *Driver) assignOutput(resource *models.Resource, client *api.Client) error {
+func (d *DeployDriver) assignOutput(resource *models.Resource, client *api.Client) error {
 	release, err := client.GetRelease(
 	release, err := client.GetRelease(
 		context.Background(),
 		context.Background(),
 		d.target.Project,
 		d.target.Project,
@@ -598,11 +615,11 @@ func (d *Driver) assignOutput(resource *models.Resource, client *api.Client) err
 	return nil
 	return nil
 }
 }
 
 
-func (d *Driver) Output() (map[string]interface{}, error) {
+func (d *DeployDriver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 	return d.output, nil
 }
 }
 
 
-func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
+func (d *DeployDriver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
 		LookupTable:  *d.lookupTable,
@@ -613,9 +630,9 @@ func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationCo
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	config := &ApplicationConfig{}
+	appConf := &ApplicationConfig{}
 
 
-	err = mapstructure.Decode(populatedConf, config)
+	err = mapstructure.Decode(populatedConf, appConf)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -623,13 +640,13 @@ func (d *Driver) getApplicationConfig(resource *models.Resource) (*ApplicationCo
 
 
 	if _, ok := resource.Config["waitForJob"]; !ok && d.source.Name == "job" {
 	if _, ok := resource.Config["waitForJob"]; !ok && d.source.Name == "job" {
 		// default to true and wait for the job to finish
 		// default to true and wait for the job to finish
-		config.WaitForJob = true
+		appConf.WaitForJob = true
 	}
 	}
 
 
-	return config, nil
+	return appConf, nil
 }
 }
 
 
-func (d *Driver) getAddonConfig(resource *models.Resource) (map[string]interface{}, error) {
+func (d *DeployDriver) getAddonConfig(resource *models.Resource) (map[string]interface{}, error) {
 	return drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 	return drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
 		LookupTable:  *d.lookupTable,
@@ -974,21 +991,21 @@ func (t *CloneEnvGroupHook) PreApply() error {
 			continue
 			continue
 		}
 		}
 
 
-		config := &ApplicationConfig{}
+		appConf := &ApplicationConfig{}
 
 
-		err := mapstructure.Decode(res.Config, &config)
+		err := mapstructure.Decode(res.Config, &appConf)
 		if err != nil {
 		if err != nil {
 			continue
 			continue
 		}
 		}
 
 
-		if config != nil && len(config.EnvGroups) > 0 {
-			target, err := preview.GetTarget(res.Target)
+		if appConf != nil && len(appConf.EnvGroups) > 0 {
+			target, err := preview.GetTarget(res.Name, res.Target)
 
 
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
 
 
-			for _, group := range config.EnvGroups {
+			for _, group := range appConf.EnvGroups {
 				if group.Name == "" {
 				if group.Name == "" {
 					return fmt.Errorf("env group name cannot be empty")
 					return fmt.Errorf("env group name cannot be empty")
 				}
 				}
@@ -1051,7 +1068,7 @@ func (t *CloneEnvGroupHook) OnConsolidatedErrors(map[string]error) {}
 func getReleaseName(res *switchboardTypes.Resource) string {
 func getReleaseName(res *switchboardTypes.Resource) string {
 	// can ignore the error because this method is called once
 	// can ignore the error because this method is called once
 	// GetTarget has alrealy been called and validated previously
 	// GetTarget has alrealy been called and validated previously
-	target, _ := preview.GetTarget(res.Target)
+	target, _ := preview.GetTarget(res.Name, res.Target)
 
 
 	if target.AppName != "" {
 	if target.AppName != "" {
 		return target.AppName
 		return target.AppName
@@ -1063,7 +1080,7 @@ func getReleaseName(res *switchboardTypes.Resource) string {
 func getReleaseType(res *switchboardTypes.Resource) string {
 func getReleaseType(res *switchboardTypes.Resource) string {
 	// can ignore the error because this method is called once
 	// can ignore the error because this method is called once
 	// GetSource has alrealy been called and validated previously
 	// GetSource has alrealy been called and validated previously
-	source, _ := preview.GetSource(res.Source)
+	source, _ := preview.GetSource(res.Name, res.Source)
 
 
 	if source != nil && source.Name != "" {
 	if source != nil && source.Name != "" {
 		return source.Name
 		return source.Name

+ 26 - 0
cli/cmd/connect.go

@@ -92,6 +92,18 @@ var connectGCRCmd = &cobra.Command{
 	},
 	},
 }
 }
 
 
+var connectGARCmd = &cobra.Command{
+	Use:   "gar",
+	Short: "Adds a GAR instance to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, runConnectGAR)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 var connectDOCRCmd = &cobra.Command{
 var connectDOCRCmd = &cobra.Command{
 	Use:   "docr",
 	Use:   "docr",
 	Short: "Adds a DOCR instance to a project",
 	Short: "Adds a DOCR instance to a project",
@@ -127,6 +139,7 @@ func init() {
 	connectCmd.AddCommand(connectRegistryCmd)
 	connectCmd.AddCommand(connectRegistryCmd)
 	connectCmd.AddCommand(connectDockerhubCmd)
 	connectCmd.AddCommand(connectDockerhubCmd)
 	connectCmd.AddCommand(connectGCRCmd)
 	connectCmd.AddCommand(connectGCRCmd)
+	connectCmd.AddCommand(connectGARCmd)
 	connectCmd.AddCommand(connectDOCRCmd)
 	connectCmd.AddCommand(connectDOCRCmd)
 	connectCmd.AddCommand(connectHelmRepoCmd)
 	connectCmd.AddCommand(connectHelmRepoCmd)
 }
 }
@@ -179,6 +192,19 @@ func runConnectGCR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _
 	return cliConf.SetRegistry(regID)
 	return cliConf.SetRegistry(regID)
 }
 }
 
 
+func runConnectGAR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
+	regID, err := connect.GAR(
+		client,
+		cliConf.Project,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return cliConf.SetRegistry(regID)
+}
+
 func runConnectDOCR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 func runConnectDOCR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
 	regID, err := connect.DOCR(
 	regID, err := connect.DOCR(
 		client,
 		client,

+ 92 - 0
cli/cmd/connect/gar.go

@@ -0,0 +1,92 @@
+package connect
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"github.com/fatih/color"
+
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+)
+
+// GAR creates a GAR integration
+func GAR(
+	client *api.Client,
+	projectID uint,
+) (uint, error) {
+	// if project ID is 0, ask the user to set the project ID or create a project
+	if projectID == 0 {
+		return 0, fmt.Errorf("no project set, please run porter config set-project")
+	}
+
+	keyFileLocation, err := utils.PromptPlaintext(fmt.Sprintf(`Please provide the full path to a service account key file.
+Key file location: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	// attempt to read the key file location
+	if info, err := os.Stat(keyFileLocation); !os.IsNotExist(err) && !info.IsDir() {
+		// read the file
+		bytes, err := ioutil.ReadFile(keyFileLocation)
+
+		if err != nil {
+			return 0, err
+		}
+
+		// create the gcp integration
+		integration, err := client.CreateGCPIntegration(
+			context.Background(),
+			projectID,
+			&types.CreateGCPRequest{
+				GCPKeyData: string(bytes),
+			},
+		)
+
+		if err != nil {
+			return 0, err
+		}
+
+		color.New(color.FgGreen).Printf("created gcp integration with id %d\n", integration.ID)
+
+		region, err := utils.PromptPlaintext(fmt.Sprintf(`Please enter the artifact registry region. For example, us-central-1.
+Artifact registry region: `))
+
+		if err != nil {
+			return 0, err
+		}
+
+		// create the registry
+		// query for registry name
+		regName, err := utils.PromptPlaintext(fmt.Sprintf(`Give this registry a name: `))
+
+		if err != nil {
+			return 0, err
+		}
+
+		reg, err := client.CreateRegistry(
+			context.Background(),
+			projectID,
+			&types.CreateRegistryRequest{
+				Name:             regName,
+				GCPIntegrationID: integration.ID,
+				URL:              region + "-docker.pkg.dev/" + integration.GCPProjectID,
+			},
+		)
+
+		if err != nil {
+			return 0, err
+		}
+
+		color.New(color.FgGreen).Printf("created registry with id %d and name %s\n", reg.ID, reg.Name)
+
+		return reg.ID, nil
+	}
+
+	return 0, fmt.Errorf("could not read service account key file")
+}

+ 2 - 2
cli/cmd/connect/gcr.go

@@ -20,7 +20,7 @@ func GCR(
 ) (uint, error) {
 ) (uint, error) {
 	// if project ID is 0, ask the user to set the project ID or create a project
 	// if project ID is 0, ask the user to set the project ID or create a project
 	if projectID == 0 {
 	if projectID == 0 {
-		return 0, fmt.Errorf("no project set, please run porter project set [id]")
+		return 0, fmt.Errorf("no project set, please run porter config set-project")
 	}
 	}
 
 
 	keyFileLocation, err := utils.PromptPlaintext(fmt.Sprintf(`Please provide the full path to a service account key file.
 	keyFileLocation, err := utils.PromptPlaintext(fmt.Sprintf(`Please provide the full path to a service account key file.
@@ -39,7 +39,7 @@ Key file location: `))
 			return 0, err
 			return 0, err
 		}
 		}
 
 
-		// create the aws integration
+		// create the gcp integration
 		integration, err := client.CreateGCPIntegration(
 		integration, err := client.CreateGCPIntegration(
 			context.Background(),
 			context.Background(),
 			projectID,
 			projectID,

+ 66 - 3
cli/cmd/deploy.go

@@ -14,6 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
+	"github.com/porter-dev/porter/cli/cmd/docker"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/porter-dev/porter/cli/cmd/utils"
 	templaterUtils "github.com/porter-dev/porter/internal/templater/utils"
 	templaterUtils "github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
@@ -148,10 +149,19 @@ for the application:
 
 
 var updatePushCmd = &cobra.Command{
 var updatePushCmd = &cobra.Command{
 	Use:   "push",
 	Use:   "push",
-	Short: "Pushes a new image for an application specified by the --app flag.",
+	Short: "Pushes an image to a Docker registry linked to your Porter project.",
+	Args:  cobra.MaximumNArgs(1),
 	Long: fmt.Sprintf(`
 	Long: fmt.Sprintf(`
 %s
 %s
 
 
+Pushes a local Docker image to a registry linked to your Porter project. This command
+requires the project ID to be set either by using the %s command
+or the --project flag. For example, to push a local nginx image:
+
+  %s
+
+%s
+
 Pushes a new image for an application specified by the --app flag. This command uses
 Pushes a new image for an application specified by the --app flag. This command uses
 the image repository saved in the application config by default. For example, if an
 the image repository saved in the application config by default. For example, if an
 application "nginx" was created from the image repo "gcr.io/snowflake-123456/nginx",
 application "nginx" was created from the image repo "gcr.io/snowflake-123456/nginx",
@@ -164,6 +174,9 @@ are using an image registry that was created outside of Porter, make sure that y
 linked it via "porter connect".
 linked it via "porter connect".
 `,
 `,
 		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update push\":"),
 		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter update push\":"),
+		color.New(color.FgBlue).Sprintf("porter config set-project"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter update push gcr.io/snowflake-123456/nginx:1234567"),
+		color.New(color.Bold).Sprintf("LEGACY USAGE:"),
 		color.New(color.FgGreen, color.Bold).Sprintf("porter update push --app nginx --tag new-tag"),
 		color.New(color.FgGreen, color.Bold).Sprintf("porter update push --app nginx --tag new-tag"),
 	),
 	),
 	Run: func(cmd *cobra.Command, args []string) {
 	Run: func(cmd *cobra.Command, args []string) {
@@ -369,8 +382,6 @@ func init() {
 
 
 	updateBuildCmd.MarkPersistentFlagRequired("app")
 	updateBuildCmd.MarkPersistentFlagRequired("app")
 
 
-	updatePushCmd.MarkPersistentFlagRequired("app")
-
 	updateConfigCmd.MarkPersistentFlagRequired("app")
 	updateConfigCmd.MarkPersistentFlagRequired("app")
 
 
 	updateEnvGroupCmd.PersistentFlags().StringVar(
 	updateEnvGroupCmd.PersistentFlags().StringVar(
@@ -490,6 +501,58 @@ func updateBuild(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 }
 }
 
 
 func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
 func updatePush(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+	if app == "" {
+		if len(args) == 0 {
+			return fmt.Errorf("please provide the docker image name")
+		}
+
+		image := args[0]
+
+		registries, err := client.ListRegistries(context.Background(), cliConf.Project)
+
+		if err != nil {
+			return err
+		}
+
+		regs := *registries
+		regID := uint(0)
+
+		for _, reg := range regs {
+			if strings.Contains(image, reg.URL) {
+				regID = reg.ID
+				break
+			}
+		}
+
+		if regID == 0 {
+			return fmt.Errorf("could not find registry for image: %s", image)
+		}
+
+		err = client.CreateRepository(context.Background(), cliConf.Project, regID,
+			&types.CreateRegistryRepositoryRequest{
+				ImageRepoURI: strings.Split(image, ":")[0],
+			},
+		)
+
+		if err != nil {
+			return err
+		}
+
+		agent, err := docker.NewAgentWithAuthGetter(client, cliConf.Project)
+
+		if err != nil {
+			return err
+		}
+
+		err = agent.PushImage(image)
+
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+
 	updateAgent, err := updateGetAgent(client)
 	updateAgent, err := updateGetAgent(client)
 
 
 	if err != nil {
 	if err != nil {

+ 5 - 0
cli/cmd/deploy/create.go

@@ -435,6 +435,11 @@ func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, err
 		}
 		}
 	}
 	}
 
 
+	if strings.Contains(imageURI, "pkg.dev") {
+		repoSlice := strings.Split(imageURI, "/")
+		imageURI = fmt.Sprintf("%s/%s", imageURI, repoSlice[len(repoSlice)-1])
+	}
+
 	return regID, imageURI, nil
 	return regID, imageURI, nil
 }
 }
 
 

+ 50 - 0
cli/cmd/docker/auth.go

@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
+	"net/url"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"regexp"
 	"regexp"
@@ -51,6 +52,8 @@ type AuthGetter struct {
 func (a *AuthGetter) GetCredentials(serverURL string) (user string, secret string, err error) {
 func (a *AuthGetter) GetCredentials(serverURL string) (user string, secret string, err error) {
 	if strings.Contains(serverURL, "gcr.io") {
 	if strings.Contains(serverURL, "gcr.io") {
 		return a.GetGCRCredentials(serverURL, a.ProjectID)
 		return a.GetGCRCredentials(serverURL, a.ProjectID)
+	} else if strings.Contains(serverURL, "pkg.dev") {
+		return a.GetGARCredentials(serverURL, a.ProjectID)
 	} else if strings.Contains(serverURL, "registry.digitalocean.com") {
 	} else if strings.Contains(serverURL, "registry.digitalocean.com") {
 		return a.GetDOCRCredentials(serverURL, a.ProjectID)
 		return a.GetDOCRCredentials(serverURL, a.ProjectID)
 	} else if strings.Contains(serverURL, "index.docker.io") {
 	} else if strings.Contains(serverURL, "index.docker.io") {
@@ -97,6 +100,53 @@ func (a *AuthGetter) GetGCRCredentials(serverURL string, projID uint) (user stri
 	return "oauth2accesstoken", token, nil
 	return "oauth2accesstoken", token, nil
 }
 }
 
 
+func (a *AuthGetter) GetGARCredentials(serverURL string, projID uint) (user string, secret string, err error) {
+	if err != nil {
+		return "", "", err
+	}
+
+	cachedEntry := a.Cache.Get(serverURL)
+
+	if !strings.HasPrefix(serverURL, "https://") {
+		serverURL = "https://" + serverURL
+	}
+
+	parsedURL, err := url.Parse(serverURL)
+
+	if err != nil {
+		return "", "", err
+	}
+
+	serverURL = parsedURL.Host + "/" + strings.Split(parsedURL.Path, "/")[0]
+
+	var token string
+
+	if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
+		token = cachedEntry.AuthorizationToken
+	} else {
+		// get a token from the server
+		tokenResp, err := a.Client.GetGARAuthorizationToken(context.Background(), projID, &types.GetRegistryGARTokenRequest{
+			ServerURL: serverURL,
+		})
+
+		if err != nil {
+			return "", "", err
+		}
+
+		token = tokenResp.Token
+
+		// set the token in cache
+		a.Cache.Set(serverURL, &AuthEntry{
+			AuthorizationToken: token,
+			RequestedAt:        time.Now(),
+			ExpiresAt:          *tokenResp.ExpiresAt,
+			ProxyEndpoint:      serverURL,
+		})
+	}
+
+	return "oauth2accesstoken", token, nil
+}
+
 func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
 func (a *AuthGetter) GetDOCRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
 	cachedEntry := a.Cache.Get(serverURL)
 	cachedEntry := a.Cache.Get(serverURL)
 
 

+ 2 - 2
cli/cmd/preview/build_image_driver.go

@@ -49,14 +49,14 @@ func NewBuildDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (
 		output:      make(map[string]interface{}),
 		output:      make(map[string]interface{}),
 	}
 	}
 
 
-	source, err := GetSource(resource.Source)
+	source, err := GetSource(resource.Name, resource.Source)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	driver.source = source
 	driver.source = source
 
 
-	target, err := GetTarget(resource.Target)
+	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 1 - 1
cli/cmd/preview/env_group_driver.go

@@ -29,7 +29,7 @@ func NewEnvGroupDriver(resource *models.Resource, opts *drivers.SharedDriverOpts
 		output:      make(map[string]interface{}),
 		output:      make(map[string]interface{}),
 	}
 	}
 
 
-	target, err := GetTarget(resource.Target)
+	target, err := GetTarget(resource.Name, resource.Target)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err

+ 1 - 1
cli/cmd/preview/push_image_driver.go

@@ -35,7 +35,7 @@ func NewPushDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (d
 		output:      make(map[string]interface{}),
 		output:      make(map[string]interface{}),
 	}
 	}
 
 
-	target, err := GetTarget(resource.Target)
+	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 2 - 2
cli/cmd/preview/update_config_driver.go

@@ -50,14 +50,14 @@ func NewUpdateConfigDriver(resource *models.Resource, opts *drivers.SharedDriver
 		output:      make(map[string]interface{}),
 		output:      make(map[string]interface{}),
 	}
 	}
 
 
-	source, err := GetSource(resource.Source)
+	source, err := GetSource(resource.Name, resource.Source)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	driver.source = source
 	driver.source = source
 
 
-	target, err := GetTarget(resource.Target)
+	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 17 - 14
cli/cmd/preview/utils.go

@@ -25,7 +25,7 @@ type Target struct {
 	Namespace string
 	Namespace string
 }
 }
 
 
-func GetSource(input map[string]interface{}) (*Source, error) {
+func GetSource(resourceName string, input map[string]interface{}) (*Source, error) {
 	output := &Source{}
 	output := &Source{}
 
 
 	// first read from env vars
 	// first read from env vars
@@ -38,21 +38,21 @@ func GetSource(input map[string]interface{}) (*Source, error) {
 		if name, ok := input["name"]; ok {
 		if name, ok := input["name"]; ok {
 			nameVal, ok := name.(string)
 			nameVal, ok := name.(string)
 			if !ok {
 			if !ok {
-				return nil, fmt.Errorf("invalid name provided")
+				return nil, fmt.Errorf("error parsing source for resource '%s': invalid name provided", resourceName)
 			}
 			}
 			output.Name = nameVal
 			output.Name = nameVal
 		}
 		}
 	}
 	}
 
 
 	if output.Name == "" {
 	if output.Name == "" {
-		return nil, fmt.Errorf("source name required")
+		return nil, fmt.Errorf("error parsing source for resource '%s': source name required", resourceName)
 	}
 	}
 
 
 	if output.Repo == "" {
 	if output.Repo == "" {
 		if repo, ok := input["repo"]; ok {
 		if repo, ok := input["repo"]; ok {
 			repoVal, ok := repo.(string)
 			repoVal, ok := repo.(string)
 			if !ok {
 			if !ok {
-				return nil, fmt.Errorf("invalid repo provided")
+				return nil, fmt.Errorf("error parsing source for resource '%s': invalid repo provided", resourceName)
 			}
 			}
 			output.Repo = repoVal
 			output.Repo = repoVal
 		}
 		}
@@ -62,7 +62,7 @@ func GetSource(input map[string]interface{}) (*Source, error) {
 		if version, ok := input["version"]; ok {
 		if version, ok := input["version"]; ok {
 			versionVal, ok := version.(string)
 			versionVal, ok := version.(string)
 			if !ok {
 			if !ok {
-				return nil, fmt.Errorf("invalid version provided")
+				return nil, fmt.Errorf("error parsing source for resource '%s': invalid version provided", resourceName)
 			}
 			}
 			output.Version = versionVal
 			output.Version = versionVal
 		}
 		}
@@ -97,7 +97,8 @@ func GetSource(input map[string]interface{}) (*Source, error) {
 			return output, nil
 			return output, nil
 		}
 		}
 
 
-		return nil, fmt.Errorf("source does not exist in any repo")
+		return nil, fmt.Errorf("error parsing source for resource '%s': source does not exist in "+
+			"'https://charts.getporter.dev' or 'https://chart-addons.getporter.dev'", resourceName)
 	} else {
 	} else {
 		// we look in the passed-in repo
 		// we look in the passed-in repo
 		values, err := existsInRepo(output.Name, output.Version, output.Repo)
 		values, err := existsInRepo(output.Name, output.Version, output.Repo)
@@ -108,17 +109,18 @@ func GetSource(input map[string]interface{}) (*Source, error) {
 		}
 		}
 	}
 	}
 
 
-	return nil, fmt.Errorf("source '%s' does not exist in repo '%s'", output.Name, output.Repo)
+	return nil, fmt.Errorf("error parsing source for resource '%s': source '%s' does not exist in repo '%s'",
+		resourceName, output.Name, output.Repo)
 }
 }
 
 
-func GetTarget(input map[string]interface{}) (*Target, error) {
+func GetTarget(resourceName string, input map[string]interface{}) (*Target, error) {
 	output := &Target{}
 	output := &Target{}
 
 
 	// first read from env vars
 	// first read from env vars
 	if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
 	if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
 		project, err := strconv.Atoi(projectEnv)
 		project, err := strconv.Atoi(projectEnv)
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error parsing target for resource '%s': %w", resourceName, err)
 		}
 		}
 		output.Project = uint(project)
 		output.Project = uint(project)
 	}
 	}
@@ -126,7 +128,7 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 	if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
 	if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
 		cluster, err := strconv.Atoi(clusterEnv)
 		cluster, err := strconv.Atoi(clusterEnv)
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("error parsing target for resource '%s': %w", resourceName, err)
 		}
 		}
 		output.Cluster = uint(cluster)
 		output.Cluster = uint(cluster)
 	}
 	}
@@ -138,7 +140,7 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 		if project, ok := input["project"]; ok {
 		if project, ok := input["project"]; ok {
 			projectVal, ok := project.(uint)
 			projectVal, ok := project.(uint)
 			if !ok {
 			if !ok {
-				return nil, fmt.Errorf("project value must be an integer")
+				return nil, fmt.Errorf("error parsing target for resource '%s': project value must be an integer", resourceName)
 			}
 			}
 			output.Project = projectVal
 			output.Project = projectVal
 		}
 		}
@@ -148,7 +150,8 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 		if cluster, ok := input["cluster"]; ok {
 		if cluster, ok := input["cluster"]; ok {
 			clusterVal, ok := cluster.(uint)
 			clusterVal, ok := cluster.(uint)
 			if !ok {
 			if !ok {
-				return nil, fmt.Errorf("cluster value must be an integer")
+				return nil, fmt.Errorf("error parsing target for resource '%s': cluster value must be an integer",
+					resourceName)
 			}
 			}
 			output.Cluster = clusterVal
 			output.Cluster = clusterVal
 		}
 		}
@@ -158,7 +161,7 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 		if namespace, ok := input["namespace"]; ok {
 		if namespace, ok := input["namespace"]; ok {
 			namespaceVal, ok := namespace.(string)
 			namespaceVal, ok := namespace.(string)
 			if !ok {
 			if !ok {
-				return nil, fmt.Errorf("invalid namespace provided")
+				return nil, fmt.Errorf("error parsing target for resource '%s': invalid namespace provided", resourceName)
 			}
 			}
 			output.Namespace = namespaceVal
 			output.Namespace = namespaceVal
 		}
 		}
@@ -167,7 +170,7 @@ func GetTarget(input map[string]interface{}) (*Target, error) {
 	if appName, ok := input["app_name"]; ok {
 	if appName, ok := input["app_name"]; ok {
 		appNameVal, ok := appName.(string)
 		appNameVal, ok := appName.(string)
 		if !ok {
 		if !ok {
-			return nil, fmt.Errorf("invalid app_name provided")
+			return nil, fmt.Errorf("error parsing target for resource '%s': invalid app_name provided", resourceName)
 		}
 		}
 		output.AppName = appNameVal
 		output.AppName = appNameVal
 	}
 	}

+ 4 - 3
cmd/app/main.go

@@ -116,9 +116,10 @@ func initData(conf *config.Config) error {
 			l.Debug().Msg("default cluster not found: attempting creation")
 			l.Debug().Msg("default cluster not found: attempting creation")
 
 
 			_, err = conf.Repo.Cluster().CreateCluster(&models.Cluster{
 			_, err = conf.Repo.Cluster().CreateCluster(&models.Cluster{
-				Name:          defaultClusterName,
-				AuthMechanism: models.InCluster,
-				ProjectID:     1,
+				Name:                defaultClusterName,
+				AuthMechanism:       models.InCluster,
+				ProjectID:           1,
+				MonitorHelmReleases: true,
 			})
 			})
 
 
 			if err != nil {
 			if err != nil {

+ 28 - 0
cmd/migrate/main.go

@@ -5,6 +5,7 @@ import (
 
 
 	"github.com/porter-dev/porter/api/server/shared/config/envloader"
 	"github.com/porter-dev/porter/api/server/shared/config/envloader"
 	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
 	"github.com/porter-dev/porter/cmd/migrate/keyrotate"
+	"github.com/porter-dev/porter/cmd/migrate/populate_source_config_display_name"
 
 
 	adapter "github.com/porter-dev/porter/internal/adapter"
 	adapter "github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 	"github.com/porter-dev/porter/internal/repository/gorm"
@@ -61,6 +62,14 @@ func main() {
 		}
 		}
 	}
 	}
 
 
+	if shouldPopulateSourceConfigDisplayName() {
+		err := populate_source_config_display_name.PopulateSourceConfigDisplayName(db, logger)
+
+		if err != nil {
+			logger.Fatal().Err(err).Msg("failed to populate source config display name")
+		}
+	}
+
 	if err := InstanceMigrate(db, envConf.DBConf); err != nil {
 	if err := InstanceMigrate(db, envConf.DBConf); err != nil {
 		logger.Fatal().Err(err).Msg("vault migration failed")
 		logger.Fatal().Err(err).Msg("vault migration failed")
 	}
 	}
@@ -83,3 +92,22 @@ func shouldKeyRotate() (bool, string, string) {
 
 
 	return c.OldEncryptionKey != "" && c.NewEncryptionKey != "", c.OldEncryptionKey, c.NewEncryptionKey
 	return c.OldEncryptionKey != "" && c.NewEncryptionKey != "", c.OldEncryptionKey, c.NewEncryptionKey
 }
 }
+
+type PopulateSourceConfigDisplayNameConf struct {
+	// we add a dummy field to avoid empty struct issue with envdecode
+	DummyField string `env:"ASDF,default=asdf"`
+
+	// if true, will populate the display name for all source configs
+	PopulateSourceConfigDisplayName bool `env:"POPULATE_SOURCE_CONFIG_DISPLAY_NAME"`
+}
+
+func shouldPopulateSourceConfigDisplayName() bool {
+	var c PopulateSourceConfigDisplayNameConf
+
+	if err := envdecode.StrictDecode(&c); err != nil {
+		log.Fatalf("Failed to decode migration conf: %s", err)
+		return false
+	}
+
+	return c.PopulateSourceConfigDisplayName
+}

+ 365 - 0
cmd/migrate/populate_source_config_display_name/helpers_test.go

@@ -0,0 +1,365 @@
+package populate_source_config_display_name_test
+
+import (
+	"fmt"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/porter-dev/porter/api/server/shared/config/env"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/adapter"
+	"github.com/porter-dev/porter/internal/encryption"
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/repository/gorm"
+	_gorm "gorm.io/gorm"
+)
+
+type tester struct {
+	Key *[32]byte
+	DB  *_gorm.DB
+
+	repo         repository.Repository
+	dbFileName   string
+	key          *[32]byte
+	initUsers    []*models.User
+	initProjects []*models.Project
+	initClusters []*models.Cluster
+	initKIs      []*ints.KubeIntegration
+	initStacks   []*models.Stack
+}
+
+func setupTestEnv(tester *tester, t *testing.T) {
+	t.Helper()
+
+	db, err := adapter.New(&env.DBConf{
+		EncryptionKey: "__random_strong_encryption_key__",
+		SQLLite:       true,
+		SQLLitePath:   tester.dbFileName,
+	})
+
+	if err != nil {
+		t.Fatalf("%\n", err)
+	}
+
+	err = db.AutoMigrate(
+		&models.Project{},
+		&models.User{},
+		&models.Cluster{},
+		&models.Stack{},
+		&models.StackEnvGroup{},
+		&models.StackSourceConfig{},
+		&models.StackRevision{},
+		&models.StackResource{},
+		&ints.KubeIntegration{},
+		&ints.ClusterTokenCache{},
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	var key [32]byte
+
+	for i, b := range []byte("__random_strong_encryption_key__") {
+		key[i] = b
+	}
+
+	tester.key = &key
+	tester.Key = &key
+	tester.DB = db
+
+	tester.repo = gorm.NewRepository(db, &key, nil)
+}
+
+func cleanup(tester *tester, t *testing.T) {
+	t.Helper()
+
+	// remove the created file file
+	os.Remove(tester.dbFileName)
+}
+
+func initUser(tester *tester, t *testing.T) {
+	t.Helper()
+
+	user := &models.User{
+		Email:    "example@example.com",
+		Password: "hello1234",
+	}
+
+	user, err := tester.repo.User().CreateUser(user)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initUsers = append(tester.initUsers, user)
+}
+
+func initCluster(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initKIs) == 0 {
+		initKubeIntegration(tester, t)
+	}
+
+	cluster := &models.Cluster{
+		ProjectID:                tester.initProjects[0].ID,
+		Name:                     "cluster-test",
+		Server:                   "https://localhost",
+		KubeIntegrationID:        tester.initKIs[0].ID,
+		CertificateAuthorityData: []byte("-----BEGIN"),
+		TokenCache: ints.ClusterTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
+		},
+	}
+
+	cluster, err := tester.repo.Cluster().CreateCluster(cluster)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initClusters = append(tester.initClusters, cluster)
+}
+
+func initProject(tester *tester, t *testing.T) {
+	t.Helper()
+
+	proj := &models.Project{
+		Name: "project-test",
+	}
+
+	proj, err := tester.repo.Project().CreateProject(proj)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initProjects = append(tester.initProjects, proj)
+}
+
+func initKubeIntegration(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	ki := &ints.KubeIntegration{
+		Mechanism:             ints.KubeLocal,
+		ProjectID:             tester.initProjects[0].ID,
+		UserID:                tester.initUsers[0].ID,
+		Kubeconfig:            []byte("current-context: testing\n"),
+		ClientCertificateData: []byte("clientcertdata"),
+		ClientKeyData:         []byte("clientkeydata"),
+		Token:                 []byte("token"),
+		Username:              []byte("username"),
+		Password:              []byte("password"),
+	}
+
+	ki, err := tester.repo.KubeIntegration().CreateKubeIntegration(ki)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initKIs = append(tester.initKIs, ki)
+}
+
+func initEmptyStack(tester *tester, t *testing.T, stackName string) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	if len(tester.initClusters) == 0 {
+		initCluster(tester, t)
+	}
+
+	uid, _ := encryption.GenerateRandomBytes(16)
+
+	// write stack to the database with creating status
+	stack := &models.Stack{
+		ProjectID: tester.initProjects[0].ID,
+		ClusterID: tester.initClusters[0].ID,
+		Namespace: "test-namespace",
+		Name:      stackName,
+		UID:       uid,
+		Revisions: []models.StackRevision{
+			{
+				RevisionNumber: 1,
+				Status:         string(types.StackRevisionStatusDeploying),
+				SourceConfigs:  []models.StackSourceConfig{},
+			},
+		},
+	}
+
+	newStack, err := tester.repo.Stack().CreateStack(stack)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initStacks = append(tester.initStacks, newStack)
+}
+
+func initStack(tester *tester, t *testing.T, stackName string) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	if len(tester.initClusters) == 0 {
+		initCluster(tester, t)
+	}
+
+	uid, _ := encryption.GenerateRandomBytes(16)
+
+	sourceConfigs := []models.StackSourceConfig{
+		{
+			Name:         "source-config-1",
+			ImageRepoURI: "some-repo",
+			ImageTag:     "some-tag",
+			UID:          uid,
+		},
+	}
+
+	// write stack to the database with creating status
+	stack := &models.Stack{
+		ProjectID: tester.initProjects[0].ID,
+		ClusterID: tester.initClusters[0].ID,
+		Namespace: "test-namespace",
+		Name:      stackName,
+		UID:       uid,
+		Revisions: []models.StackRevision{
+			{
+				RevisionNumber: 1,
+				Status:         string(types.StackRevisionStatusDeploying),
+				SourceConfigs:  sourceConfigs,
+			},
+		},
+	}
+
+	newStack, err := tester.repo.Stack().CreateStack(stack)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initStacks = append(tester.initStacks, newStack)
+}
+
+func createNewStackRevision(tester *tester, t *testing.T, stackName string) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+	if len(tester.initClusters) == 0 {
+		initCluster(tester, t)
+	}
+	if len(tester.initStacks) == 0 {
+		initStack(tester, t, stackName)
+	}
+
+	stack := tester.initStacks[0]
+
+	for _, s := range tester.initStacks {
+		if s.Name == stackName {
+			stack = s
+			break
+		}
+	}
+
+	prevRevision := findLatestRevisionByRevisionNumber(t, stack.Revisions)
+
+	oldSourceConfig := prevRevision.SourceConfigs[0]
+
+	newUid, _ := encryption.GenerateRandomBytes(16)
+	sourceConfigs := []models.StackSourceConfig{
+		{
+			Name:         oldSourceConfig.Name,
+			ImageRepoURI: "some-repo-" + fmt.Sprint(prevRevision.RevisionNumber+1),
+			ImageTag:     "some-tag-" + fmt.Sprint(prevRevision.RevisionNumber+1),
+			UID:          newUid,
+		},
+	}
+
+	newRevision := models.StackRevision{
+		RevisionNumber: prevRevision.RevisionNumber + 1,
+		Status:         string(types.StackRevisionStatusDeploying),
+		SourceConfigs:  sourceConfigs,
+		StackID:        stack.ID,
+	}
+
+	tester.repo.Stack().AppendNewRevision(&newRevision)
+}
+
+func findLatestRevisionByRevisionNumber(t *testing.T, revisions []models.StackRevision) *models.StackRevision {
+	t.Helper()
+
+	latestRevision := revisions[0]
+	for _, revision := range revisions {
+		if revision.RevisionNumber > latestRevision.RevisionNumber {
+			latestRevision = revision
+		}
+	}
+
+	return &latestRevision
+}
+
+func appendNewSourceConfig(t *testing.T, tester *tester, stack *models.Stack, sourceConfig models.StackSourceConfig) {
+	t.Helper()
+
+	prevRevision := findLatestRevisionByRevisionNumber(t, stack.Revisions)
+
+	previousSourceConfigs := []models.StackSourceConfig{}
+
+	for _, sourceConfig := range prevRevision.SourceConfigs {
+		newUid, _ := encryption.GenerateRandomBytes(16)
+
+		sc := models.StackSourceConfig{
+			Name:         sourceConfig.Name,
+			ImageRepoURI: sourceConfig.ImageRepoURI,
+			ImageTag:     sourceConfig.ImageTag,
+			UID:          newUid,
+		}
+		previousSourceConfigs = append(previousSourceConfigs, sc)
+	}
+
+	newRevision := models.StackRevision{
+		RevisionNumber: prevRevision.RevisionNumber + 1,
+		Status:         string(types.StackRevisionStatusDeploying),
+		SourceConfigs:  append(prevRevision.SourceConfigs, sourceConfig),
+		StackID:        stack.ID,
+	}
+
+	tester.repo.Stack().AppendNewRevision(&newRevision)
+}

+ 40 - 0
cmd/migrate/populate_source_config_display_name/populate.go

@@ -0,0 +1,40 @@
+package populate_source_config_display_name
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	lr "github.com/porter-dev/porter/pkg/logger"
+	_gorm "gorm.io/gorm"
+)
+
+func PopulateSourceConfigDisplayName(db *_gorm.DB, logger *lr.Logger) error {
+	logger.Info().Msg("Initiated source config display name population")
+	// get all source configs
+	sourceConfigs := make([]*models.StackSourceConfig, 0)
+
+	if err := db.Find(&sourceConfigs).Error; err != nil {
+		logger.Error().Msgf("failed to get source configs %v", err)
+		return err
+	}
+
+	if len(sourceConfigs) == 0 {
+		logger.Info().Msg("no source configs to populate")
+		return nil
+	}
+
+	updatedCount := 0
+	// copy name to display name if display name is empty
+	for _, sourceConfig := range sourceConfigs {
+		if sourceConfig.DisplayName == "" {
+			sourceConfig.DisplayName = sourceConfig.Name
+			updatedCount++
+		}
+	}
+	// update source configs
+	if err := db.Save(&sourceConfigs).Error; err != nil {
+		logger.Error().Msgf("failed to update source configs %v", err)
+		return err
+	}
+
+	logger.Info().Msgf("source config display name population completed, %d source configs updated", updatedCount)
+	return nil
+}

+ 76 - 0
cmd/migrate/populate_source_config_display_name/populate_test.go

@@ -0,0 +1,76 @@
+package populate_source_config_display_name_test
+
+import (
+	"testing"
+
+	"github.com/porter-dev/porter/cmd/migrate/populate_source_config_display_name"
+
+	"github.com/porter-dev/porter/internal/models"
+	lr "github.com/porter-dev/porter/pkg/logger"
+)
+
+func TestAllSourceConfigsArePopulated(t *testing.T) {
+	logger := lr.NewConsole(true)
+
+	tester := &tester{
+		dbFileName: "./porter_stable_source_config_id_population.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	defer cleanup(tester, t)
+
+	stackName := "first-stack"
+
+	initStack(tester, t, stackName)
+
+	createNewStackRevision(tester, t, stackName)
+
+	createNewStackRevision(tester, t, stackName)
+
+	createNewStackRevision(tester, t, stackName)
+
+	err := populate_source_config_display_name.PopulateSourceConfigDisplayName(tester.DB, logger)
+
+	if err != nil {
+		t.Fatalf("%\n", err)
+		return
+	}
+
+	sourceConfigs := []*models.StackSourceConfig{}
+
+	if err := tester.DB.Find(&sourceConfigs).Error; err != nil {
+		t.Fatalf("failed to find source configs: %s", err)
+	}
+
+	if len(sourceConfigs) != 4 {
+		t.Fatalf("expected 4 source configs, got %d", len(sourceConfigs))
+	}
+
+	for _, sc := range sourceConfigs {
+		if sc.DisplayName == "" {
+			t.Fatalf("expected display name to be populated, got empty string")
+		}
+	}
+}
+
+func TestPopulateOnEmptyStack(t *testing.T) {
+	logger := lr.NewConsole(true)
+
+	tester := &tester{
+		dbFileName: "./porter_stable_source_config_id_population.db",
+	}
+
+	setupTestEnv(tester, t)
+
+	initEmptyStack(tester, t, "empty-stack")
+
+	defer cleanup(tester, t)
+
+	err := populate_source_config_display_name.PopulateSourceConfigDisplayName(tester.DB, logger)
+
+	if err != nil {
+		t.Fatalf("expected no error, got %s", err)
+		return
+	}
+}

+ 8 - 0
dashboard/src/components/porter-form/FormDebugger.tsx

@@ -11,6 +11,7 @@ import "ace-builds/src-noconflict/mode-text";
 
 
 import Heading from "../form-components/Heading";
 import Heading from "../form-components/Heading";
 import Helper from "../form-components/Helper";
 import Helper from "../form-components/Helper";
+import { ChartType } from "shared/types";
 
 
 type PropsType = {
 type PropsType = {
   goBack: () => void;
   goBack: () => void;
@@ -170,6 +171,13 @@ export default class FormDebugger extends Component<PropsType, StateType> {
           rightTabOptions={this.state.showBonusTabs ? tabOptions : []}
           rightTabOptions={this.state.showBonusTabs ? tabOptions : []}
           renderTabContents={this.renderTabContents}
           renderTabContents={this.renderTabContents}
           saveButtonText={"Test Submit"}
           saveButtonText={"Test Submit"}
+          injectedProps={{
+            "url-link": {
+              chart: {
+                name: "something",
+              } as ChartType,
+            },
+          }}
         />
         />
       </StyledFormDebugger>
       </StyledFormDebugger>
     );
     );

+ 4 - 0
dashboard/src/components/porter-form/PorterForm.tsx

@@ -12,6 +12,7 @@ import {
   SelectField,
   SelectField,
   ServiceIPListField,
   ServiceIPListField,
   TextAreaField,
   TextAreaField,
+  UrlLinkField,
 } from "./types";
 } from "./types";
 import TabRegion, { TabOption } from "../TabRegion";
 import TabRegion, { TabOption } from "../TabRegion";
 import Heading from "../form-components/Heading";
 import Heading from "../form-components/Heading";
@@ -29,6 +30,7 @@ import ResourceList from "./field-components/ResourceList";
 import VeleroForm from "./field-components/VeleroForm";
 import VeleroForm from "./field-components/VeleroForm";
 import CronInput from "./field-components/CronInput";
 import CronInput from "./field-components/CronInput";
 import TextAreaInput from "./field-components/TextAreaInput";
 import TextAreaInput from "./field-components/TextAreaInput";
+import UrlLink from "./field-components/UrlLink";
 
 
 interface Props {
 interface Props {
   leftTabOptions?: TabOption[];
   leftTabOptions?: TabOption[];
@@ -98,6 +100,8 @@ const PorterForm: React.FC<Props> = (props) => {
         return <CronInput {...(bundledProps as CronField)} />;
         return <CronInput {...(bundledProps as CronField)} />;
       case "text-area":
       case "text-area":
         return <TextAreaInput {...(bundledProps as TextAreaField)} />;
         return <TextAreaInput {...(bundledProps as TextAreaField)} />;
+      case "url-link":
+        return <UrlLink {...(bundledProps as UrlLinkField)} />;
     }
     }
     return <p>Not Implemented: {(field as any).type}</p>;
     return <p>Not Implemented: {(field as any).type}</p>;
   };
   };

+ 94 - 0
dashboard/src/components/porter-form/field-components/UrlLink.tsx

@@ -0,0 +1,94 @@
+import { get } from "lodash";
+import React from "react";
+import styled from "styled-components";
+import { UrlLinkField } from "../types";
+import { hasSetValue } from "../utils";
+
+const populate = (str: string, obj: unknown) => {
+  return str.replace(/{[^{}]*}+/g, (match) => {
+    const key = match.replace("{", "").replace("}", "");
+    let value;
+    if (key[0] === ".") {
+      value = get(obj, key.substring(1));
+    } else {
+      value = get(obj, key);
+    }
+
+    if (typeof value !== "string") {
+      return "Couldn't find value " + key;
+    }
+
+    return value;
+  });
+};
+
+const UrlLink = (props: UrlLinkField) => {
+  const { value, label, injectedProps } = props;
+
+  if (!hasSetValue(props)) {
+    return null;
+  }
+
+  let val = value;
+
+  if (Array.isArray(value)) {
+    val = value[0];
+  }
+
+  if (typeof val !== "string") {
+    return null;
+  }
+
+  if (!injectedProps?.chart) {
+    return null;
+  }
+
+  const populatedUrl = populate(val, injectedProps.chart);
+
+  return (
+    <>
+      <Label>{label}</Label>
+      <StyledServiceRow>
+        <a href={populatedUrl} target="_blank">
+          <i className="material-icons-outlined">link</i>
+          {populatedUrl}
+        </a>
+      </StyledServiceRow>
+    </>
+  );
+};
+
+export default UrlLink;
+
+const StyledServiceRow = styled.div`
+  width: 100%;
+  height: 40px;
+  background: #ffffff11;
+  margin-bottom: 15px;
+  border-radius: 5px;
+  padding: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  > a {
+    margin-left: 2px;
+    font-size: 13px;
+    user-select: text;
+    display: flex;
+    -webkit-box-align: center;
+    align-items: center;
+    > i {
+      font-size: 15px;
+      margin-right: 10px;
+    }
+  }
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+`;

+ 11 - 2
dashboard/src/components/porter-form/types.ts

@@ -5,7 +5,7 @@
 
 
 // YAML Field interfaces
 // YAML Field interfaces
 
 
-import { ContextProps } from "../../shared/types";
+import { ChartType, ContextProps } from "../../shared/types";
 
 
 export interface GenericField {
 export interface GenericField {
   id: string;
   id: string;
@@ -146,6 +146,14 @@ export interface TextAreaField extends GenericInputField {
   };
   };
 }
 }
 
 
+export interface UrlLinkField extends GenericInputField {
+  type: "url-link";
+  label: string;
+  injectedProps: {
+    chart: ChartType;
+  };
+}
+
 export type FormField =
 export type FormField =
   | HeadingField
   | HeadingField
   | SubtitleField
   | SubtitleField
@@ -159,7 +167,8 @@ export type FormField =
   | VeleroBackupField
   | VeleroBackupField
   | VariableField
   | VariableField
   | CronField
   | CronField
-  | TextAreaField;
+  | TextAreaField
+  | UrlLinkField;
 
 
 export interface ShowIfAnd {
 export interface ShowIfAnd {
   and: ShowIf[];
   and: ShowIf[];

+ 3 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -870,6 +870,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
                                 ? stackEnvGroups
                                 ? stackEnvGroups
                                 : undefined,
                                 : undefined,
                           },
                           },
+                          "url-link": {
+                            chart: currentChart,
+                          },
                         }}
                         }}
                       />
                       />
                     </BodyWrapper>
                     </BodyWrapper>

+ 3 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -395,6 +395,9 @@ export const ExpandedJobChartFC: React.FC<{
                   availableSyncEnvGroups:
                   availableSyncEnvGroups:
                     isStack && !disableForm ? stackEnvGroups : undefined,
                     isStack && !disableForm ? stackEnvGroups : undefined,
                 },
                 },
+                "url-link": {
+                  chart: chart,
+                },
               }}
               }}
             />
             />
           )}
           )}

+ 5 - 1
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx

@@ -218,7 +218,11 @@ const ExpandedStack = () => {
             component: (
             component: (
               <>
               <>
                 <Gap></Gap>
                 <Gap></Gap>
-                <Settings stackName={stack.name} onDelete={handleDelete} />
+                <Settings
+                  stack={stack}
+                  onDelete={handleDelete}
+                  onUpdate={refreshStack}
+                />
               </>
               </>
             ),
             ),
           },
           },

+ 114 - 73
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx

@@ -1,11 +1,9 @@
-import { Tooltip } from "@material-ui/core";
-import ImageSelector from "components/image-selector/ImageSelector";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
-import React, { useContext, useMemo, useState } from "react";
+import React, { useContext, useReducer, useRef, useState } from "react";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import styled from "styled-components";
 import styled from "styled-components";
-import { AppResource, FullStackRevision, SourceConfig, Stack } from "../types";
+import { FullStackRevision, SourceConfig } from "../types";
 import SourceEditorDocker from "./components/SourceEditorDocker";
 import SourceEditorDocker from "./components/SourceEditorDocker";
 
 
 const _SourceConfig = ({
 const _SourceConfig = ({
@@ -32,7 +30,12 @@ const _SourceConfig = ({
     const index = newSourceConfigArray.findIndex(
     const index = newSourceConfigArray.findIndex(
       (sc) => sc.id === sourceConfig.id
       (sc) => sc.id === sourceConfig.id
     );
     );
-    newSourceConfigArray[index] = sourceConfig;
+
+    newSourceConfigArray[index] = {
+      ...sourceConfig,
+      display_name: sourceConfig.display_name || sourceConfig.name,
+    };
+
     setSourceConfigArrayCopy(newSourceConfigArray);
     setSourceConfigArrayCopy(newSourceConfigArray);
   };
   };
 
 
@@ -64,39 +67,13 @@ const _SourceConfig = ({
   return (
   return (
     <SourceConfigStyles.Wrapper>
     <SourceConfigStyles.Wrapper>
       {revision.source_configs.map((sourceConfig) => {
       {revision.source_configs.map((sourceConfig) => {
-        const apps = getAppsFromSourceConfig(revision.resources, sourceConfig);
-
-        const appList = formatAppList(apps, 2);
         return (
         return (
-          <SourceConfigStyles.ItemContainer>
-            {appList.hiddenApps?.length ? (
-              <Tooltip
-                title={
-                  <>
-                    {appList.hiddenApps.map((appName) => (
-                      <SourceConfigStyles.TooltipItem>
-                        {appName}
-                      </SourceConfigStyles.TooltipItem>
-                    ))}
-                  </>
-                }
-                placement={"bottom-end"}
-              >
-                <SourceConfigStyles.ItemTitle>
-                  Used by {appList.value}
-                </SourceConfigStyles.ItemTitle>
-              </Tooltip>
-            ) : (
-              <SourceConfigStyles.ItemTitle>
-                Used by {appList.value}
-              </SourceConfigStyles.ItemTitle>
-            )}
-            <SourceEditorDocker
-              sourceConfig={sourceConfig}
-              onChange={handleChange}
-              readOnly={readOnly || buttonStatus === "loading"}
-            />
-          </SourceConfigStyles.ItemContainer>
+          <SourceConfigItem
+            sourceConfig={sourceConfig}
+            key={sourceConfig.id}
+            handleChange={handleChange}
+            disabled={readOnly || buttonStatus === "loading"}
+          />
         );
         );
       })}
       })}
       {readOnly ? null : (
       {readOnly ? null : (
@@ -117,41 +94,6 @@ const _SourceConfig = ({
 
 
 export default _SourceConfig;
 export default _SourceConfig;
 
 
-const getAppsFromSourceConfig = (
-  apps: AppResource[],
-  sourceConfig: SourceConfig
-) => {
-  return apps.filter((app) => {
-    return app.stack_source_config.id === sourceConfig.id;
-  });
-};
-
-const formatAppList = (apps: AppResource[], limit: number = 3) => {
-  if (apps.length <= limit) {
-    const formatter = new Intl.ListFormat("en", {
-      style: "long",
-      type: "conjunction",
-    });
-    return {
-      value: formatter.format(apps.map((app) => app.name)),
-      hiddenApps: [],
-    };
-  }
-
-  const hiddenApps = [...apps]
-    .splice(limit, apps.length)
-    .map((app) => app.name);
-
-  return {
-    value: apps
-      .map((app) => app.name)
-      .splice(0, limit)
-      .join(", ")
-      .concat(` and ${apps.length - limit} more`),
-    hiddenApps,
-  };
-};
-
 const SourceConfigStyles = {
 const SourceConfigStyles = {
   Wrapper: styled.div`
   Wrapper: styled.div`
     margin-top: 30px;
     margin-top: 30px;
@@ -164,8 +106,17 @@ const SourceConfigStyles = {
   `,
   `,
   ItemTitle: styled.div`
   ItemTitle: styled.div`
     font-size: 16px;
     font-size: 16px;
-    width: fit-content;
     font-weight: 500;
     font-weight: 500;
+
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 10px;
+    > span {
+      overflow-x: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
   `,
   `,
   TooltipItem: styled.div`
   TooltipItem: styled.div`
     font-size: 14px;
     font-size: 14px;
@@ -179,3 +130,93 @@ const SourceConfigStyles = {
     z-index: unset;
     z-index: unset;
   `,
   `,
 };
 };
+
+const SourceConfigItem = ({
+  sourceConfig,
+  handleChange,
+  disabled,
+}: {
+  sourceConfig: SourceConfig;
+  handleChange: (sourceConfig: SourceConfig) => void;
+  disabled: boolean;
+}) => {
+  const [editNameMode, toggleEditNameMode] = useReducer((prev) => !prev, false);
+  const prevName = useRef(sourceConfig.display_name || sourceConfig.name);
+  const [name, setName] = useState(
+    sourceConfig.display_name || sourceConfig.name
+  );
+
+  const handleNameChange = (newName: string) => {
+    setName(newName);
+    handleChange({ ...sourceConfig, display_name: newName });
+  };
+
+  const handleNameChangeCancel = () => {
+    setName(prevName.current);
+    handleChange({ ...sourceConfig, display_name: prevName.current });
+    toggleEditNameMode();
+  };
+
+  return (
+    <SourceConfigStyles.ItemContainer>
+      {editNameMode && !disabled ? (
+        <>
+          <SourceConfigStyles.ItemTitle>
+            <PlainTextInput
+              value={name}
+              onChange={(e) => handleNameChange(e.target.value)}
+              type="text"
+              disabled={disabled}
+            />
+            <EditButton onClick={handleNameChangeCancel}>
+              <i className="material-icons-outlined">close</i>
+            </EditButton>
+          </SourceConfigStyles.ItemTitle>
+        </>
+      ) : (
+        <SourceConfigStyles.ItemTitle>
+          <span>{name}</span>
+
+          <EditButton onClick={toggleEditNameMode}>
+            <i className="material-icons-outlined">edit</i>
+          </EditButton>
+        </SourceConfigStyles.ItemTitle>
+      )}
+
+      <SourceEditorDocker
+        sourceConfig={sourceConfig}
+        onChange={handleChange}
+        readOnly={disabled}
+      />
+    </SourceConfigStyles.ItemContainer>
+  );
+};
+
+const EditButton = styled.button`
+  outline: none;
+  cursor: pointer;
+  color: white;
+  border: 1px solid rgba(255, 255, 255, 0.333);
+  background: rgba(255, 255, 255, 0.067);
+  height: 35px;
+  width: 35px;
+  border-radius: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  > i {
+    font-size: 20px;
+  }
+`;
+
+const PlainTextInput = styled.input`
+  outline: none;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  font-size: 13px;
+  background: #ffffff11;
+  width: 100%;
+  color: white;
+  padding: 5px 10px;
+  height: 35px;
+`;

+ 67 - 5
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx

@@ -1,16 +1,30 @@
 import Heading from "components/form-components/Heading";
 import Heading from "components/form-components/Heading";
-import React, { useContext } from "react";
+import Helper from "components/form-components/Helper";
+import InputRow from "components/form-components/InputRow";
+import React, { useContext, useState } from "react";
+import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import styled from "styled-components";
 import styled from "styled-components";
+import { SubmitButton } from "../../launch/components/styles";
+import { Stack } from "../../types";
 
 
 const Settings = ({
 const Settings = ({
-  stackName,
+  stack,
   onDelete,
   onDelete,
+  onUpdate,
 }: {
 }: {
-  stackName: string;
+  stack: Stack;
   onDelete: () => void;
   onDelete: () => void;
+  onUpdate: () => Promise<void>;
 }) => {
 }) => {
-  const { setCurrentOverlay } = useContext(Context);
+  const {
+    currentCluster,
+    currentProject,
+    setCurrentOverlay,
+    setCurrentError,
+  } = useContext(Context);
+  const [stackName, setStackName] = useState(stack.name);
+  const [buttonStatus, setButtonStatus] = useState("");
 
 
   const handleDelete = () => {
   const handleDelete = () => {
     setCurrentOverlay({
     setCurrentOverlay({
@@ -22,10 +36,54 @@ const Settings = ({
       onNo: () => setCurrentOverlay(null),
       onNo: () => setCurrentOverlay(null),
     });
     });
   };
   };
+
+  const handleStackNameChange = async () => {
+    setButtonStatus("loading");
+    try {
+      await api.updateStack(
+        "<token>",
+        {
+          name: stackName,
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+          stack_id: stack.id,
+          namespace: stack.namespace,
+        }
+      );
+      await onUpdate();
+      setButtonStatus("successful");
+    } catch (err) {
+      setCurrentError(err);
+      setButtonStatus("Couldn't update the stack name. Try again later.");
+    }
+  };
+
   return (
   return (
     <Wrapper>
     <Wrapper>
       <StyledSettingsSection>
       <StyledSettingsSection>
-        <Heading>Settings</Heading>
+        <Heading>Update Stack name</Heading>
+
+        <InputRow
+          label="Stack name"
+          value={stackName}
+          setValue={setStackName as any}
+          type="text"
+          width="300px"
+        />
+        <SaveButton
+          text="Update"
+          onClick={handleStackNameChange}
+          disabled={stackName === stack.name}
+          makeFlush
+          clearPosition
+          statusPosition="right"
+          status={buttonStatus}
+        ></SaveButton>
+
+        <Heading>Additional Settings</Heading>
+
         <Button color="#b91133" onClick={handleDelete}>
         <Button color="#b91133" onClick={handleDelete}>
           Delete stack
           Delete stack
         </Button>
         </Button>
@@ -36,6 +94,10 @@ const Settings = ({
 
 
 export default Settings;
 export default Settings;
 
 
+const SaveButton = styled(SubmitButton)`
+  justify-content: flex-start;
+`;
+
 const Wrapper = styled.div`
 const Wrapper = styled.div`
   width: 100%;
   width: 100%;
   padding-bottom: 65px;
   padding-bottom: 65px;

+ 20 - 2
dashboard/src/main/home/cluster-dashboard/stacks/launch/SelectSource.tsx

@@ -8,10 +8,11 @@ import Helper from "components/form-components/Helper";
 import Heading from "components/form-components/Heading";
 import Heading from "components/form-components/Heading";
 import styled from "styled-components";
 import styled from "styled-components";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
+import InputRow from "components/form-components/InputRow";
 
 
 const SelectSource = () => {
 const SelectSource = () => {
   const { addSourceConfig } = useContext(StacksLaunchContext);
   const { addSourceConfig } = useContext(StacksLaunchContext);
-
+  const [sourceName, setSourceName] = useState("");
   const [imageUrl, setImageUrl] = useState("");
   const [imageUrl, setImageUrl] = useState("");
   const [imageTag, setImageTag] = useState("");
   const [imageTag, setImageTag] = useState("");
   const { pushFiltered } = useRouting();
   const { pushFiltered } = useRouting();
@@ -22,6 +23,7 @@ const SelectSource = () => {
     }
     }
 
 
     const newSource: Omit<CreateStackBody["source_configs"][0], "name"> = {
     const newSource: Omit<CreateStackBody["source_configs"][0], "name"> = {
+      display_name: sourceName,
       image_repo_uri: imageUrl,
       image_repo_uri: imageUrl,
       image_tag: imageTag,
       image_tag: imageTag,
     };
     };
@@ -39,11 +41,23 @@ const SelectSource = () => {
         New Application Stack
         New Application Stack
       </TitleSection>
       </TitleSection>
       <Heading>Stack Source</Heading>
       <Heading>Stack Source</Heading>
+
+      <Br />
+      <InputRowWrapper>
+        <InputRow
+          label="Source Name"
+          value={sourceName}
+          setValue={(val) => setSourceName(val as string)}
+          type="text"
+          width="100%"
+          placeholder="Leave empty for auto-generated source config name"
+        />
+      </InputRowWrapper>
+
       <Helper>
       <Helper>
         Specify a source to deploy all stack applications from:
         Specify a source to deploy all stack applications from:
         <Required>*</Required>
         <Required>*</Required>
       </Helper>
       </Helper>
-      <Br />
       <ImageSelector
       <ImageSelector
         selectedImageUrl={imageUrl}
         selectedImageUrl={imageUrl}
         setSelectedImageUrl={setImageUrl}
         setSelectedImageUrl={setImageUrl}
@@ -86,3 +100,7 @@ const Polymer = styled.div`
     margin-right: 18px;
     margin-right: 18px;
   }
   }
 `;
 `;
+
+const InputRowWrapper = styled.div`
+  width: 60%;
+`;

+ 4 - 1
dashboard/src/main/home/cluster-dashboard/stacks/launch/Store.tsx

@@ -96,8 +96,11 @@ const StacksLaunchContextProvider: React.FC<{}> = ({ children }) => {
       source_configs: [
       source_configs: [
         ...prev.source_configs,
         ...prev.source_configs,
         {
         {
-          name: newSourceConfigName(prev.source_configs.length),
           ...sourceConfig,
           ...sourceConfig,
+          display_name:
+            sourceConfig.display_name ||
+            newSourceConfigName(prev.source_configs.length),
+          name: newSourceConfigName(prev.source_configs.length),
         },
         },
       ],
       ],
     }));
     }));

+ 2 - 0
dashboard/src/main/home/cluster-dashboard/stacks/types.ts

@@ -9,6 +9,7 @@ export type CreateStackBody = {
     values: unknown;
     values: unknown;
   }[];
   }[];
   source_configs: {
   source_configs: {
+    display_name: string;
     name: string;
     name: string;
     image_repo_uri: string;
     image_repo_uri: string;
     image_tag: string;
     image_tag: string;
@@ -80,6 +81,7 @@ export type StackRevision = {
 
 
 export type SourceConfig = {
 export type SourceConfig = {
   id: string;
   id: string;
+  display_name: string;
   name: string;
   name: string;
   created_at: string;
   created_at: string;
   updated_at: string;
   updated_at: string;

+ 3 - 0
dashboard/src/main/home/integrations/create-integration/CreateIntegrationForm.tsx

@@ -8,6 +8,7 @@ import EKSForm from "./EKSForm";
 import GCRForm from "./GCRForm";
 import GCRForm from "./GCRForm";
 import ECRForm from "./ECRForm";
 import ECRForm from "./ECRForm";
 import GitlabForm from "./GitlabForm";
 import GitlabForm from "./GitlabForm";
+import GARForm from "./GARForm";
 
 
 type PropsType = {
 type PropsType = {
   integrationName: string;
   integrationName: string;
@@ -34,6 +35,8 @@ export default class CreateIntegrationForm extends Component<
         return <ECRForm closeForm={this.props.closeForm} />;
         return <ECRForm closeForm={this.props.closeForm} />;
       case "gcr":
       case "gcr":
         return <GCRForm closeForm={this.props.closeForm} />;
         return <GCRForm closeForm={this.props.closeForm} />;
+      case "gar":
+        return <GARForm closeForm={this.props.closeForm} />;
       case "gitlab":
       case "gitlab":
         return <GitlabForm closeForm={this.props.closeForm} />;
         return <GitlabForm closeForm={this.props.closeForm} />;
       default:
       default:

+ 156 - 0
dashboard/src/main/home/integrations/create-integration/GARForm.tsx

@@ -0,0 +1,156 @@
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
+import InputRow from "components/form-components/InputRow";
+import SelectRow from "components/form-components/SelectRow";
+import UploadArea from "components/form-components/UploadArea";
+import SaveButton from "components/SaveButton";
+import { GCP_REGION_OPTIONS } from "main/home/onboarding/constants";
+import React, { useContext, useState } from "react";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import styled from "styled-components";
+
+type GCPIntegration = {
+  id: string;
+  gcp_project_id: string;
+  [key: string]: unknown;
+};
+
+const GARForm = (props: { closeForm: () => void }) => {
+  const { closeForm } = props;
+  const { currentProject } = useContext(Context);
+
+  const [credentialsName, setCredentialsName] = useState("");
+  const [serviceAccountKey, setServiceAccountKey] = useState("");
+  const [region, setRegion] = useState("us-east1");
+  const [buttonStatus, setButtonStatus] = useState("");
+
+  const isValid = () => {
+    return (
+      credentialsName.length > 0 &&
+      serviceAccountKey.length > 0 &&
+      region.length > 0
+    );
+  };
+
+  const handleSubmit = async () => {
+    setButtonStatus("loading");
+
+    let integration: GCPIntegration;
+
+    try {
+      const res = await api.createGCPIntegration<GCPIntegration>(
+        "<token>",
+        {
+          gcp_key_data: serviceAccountKey,
+          gcp_project_id: "",
+        },
+        {
+          project_id: currentProject.id,
+        }
+      );
+
+      integration = res.data;
+    } catch (error) {
+      setButtonStatus(
+        "Couldn't connect with GCP with the provided credentials."
+      );
+      return;
+    }
+
+    try {
+      await api.connectGCRRegistry(
+        "token",
+        {
+          gcp_integration_id: integration.id,
+          name: credentialsName,
+          url: `${region}-docker.pkg.dev/${integration.gcp_project_id}`,
+        },
+        { id: currentProject.id }
+      );
+    } catch (error) {
+      setButtonStatus(
+        "Couldn't connect the GAR registry with the provided credentials."
+      );
+      return;
+    }
+
+    setButtonStatus("successfull");
+    closeForm();
+  };
+
+  return (
+    <StyledForm>
+      <CredentialWrapper>
+        <Heading>Porter Settings</Heading>
+        <Helper>
+          Give a name to this set of registry credentials (just for Porter).
+        </Helper>
+        <InputRow
+          type="text"
+          value={credentialsName}
+          setValue={(credentialsName: string) =>
+            setCredentialsName(credentialsName)
+          }
+          isRequired={true}
+          label="🏷️ Registry Name"
+          placeholder="ex: paper-straw"
+          width="100%"
+        />
+        <Heading>GCP Settings</Heading>
+        <Helper>Service account credentials for GCP permissions.</Helper>
+        <UploadArea
+          setValue={(x: any) => setServiceAccountKey(x)}
+          label="🔒 GCP Key Data (JSON)"
+          placeholder="Choose a file or drag it here."
+          width="100%"
+          height="100%"
+          isRequired={true}
+        />
+        <Helper>GAR Region</Helper>
+        <SelectRow
+          options={GCP_REGION_OPTIONS}
+          width="100%"
+          value={region}
+          scrollBuffer={true}
+          dropdownMaxHeight="240px"
+          setActiveValue={(x: string) => {
+            setRegion(x);
+          }}
+          label="📍 GCP Region"
+        />
+      </CredentialWrapper>
+      <SaveButton
+        text="Save Settings"
+        status={buttonStatus}
+        makeFlush={true}
+        disabled={!isValid()}
+        onClick={!isValid() ? null : handleSubmit}
+      />
+    </StyledForm>
+  );
+};
+
+export default GARForm;
+
+const CredentialWrapper = styled.div`
+  padding: 5px 40px 25px;
+  background: #ffffff11;
+  border-radius: 5px;
+`;
+
+const StyledForm = styled.div`
+  position: relative;
+  padding-bottom: 75px;
+`;
+
+const CodeBlock = styled.span`
+  display: inline-block;
+  background-color: #1b1d26;
+  color: white;
+  border-radius: 5px;
+  font-family: monospace;
+  padding: 2px 3px;
+  margin-top: -2px;
+  user-select: text;
+`;

+ 6 - 0
dashboard/src/main/home/onboarding/components/ProviderSelector.tsx

@@ -32,6 +32,11 @@ export const registryOptions = [
     icon: integrationList["gcr"]?.icon,
     icon: integrationList["gcr"]?.icon,
     label: "Google Cloud Registry (GCR)",
     label: "Google Cloud Registry (GCR)",
   },
   },
+  {
+    value: "gar",
+    icon: integrationList["gcr"]?.icon,
+    label: "Google Artifact Registry (GAR)",
+  },
   {
   {
     value: "do",
     value: "do",
     icon: integrationList["do"]?.icon,
     icon: integrationList["do"]?.icon,
@@ -50,6 +55,7 @@ export const provisionerOptions = [
     icon: integrationList["gcp"]?.icon,
     icon: integrationList["gcp"]?.icon,
     label: "Google Cloud Platform (GCP)",
     label: "Google Cloud Platform (GCP)",
   },
   },
+
   {
   {
     value: "do",
     value: "do",
     icon: integrationList["do"]?.icon,
     icon: integrationList["do"]?.icon,

+ 26 - 0
dashboard/src/main/home/onboarding/constants.ts

@@ -0,0 +1,26 @@
+export const GCP_REGION_OPTIONS = [
+  { value: "asia-east1", label: "asia-east1" },
+  { value: "asia-east2", label: "asia-east2" },
+  { value: "asia-northeast1", label: "asia-northeast1" },
+  { value: "asia-northeast2", label: "asia-northeast2" },
+  { value: "asia-northeast3", label: "asia-northeast3" },
+  { value: "asia-south1", label: "asia-south1" },
+  { value: "asia-southeast1", label: "asia-southeast1" },
+  { value: "asia-southeast2", label: "asia-southeast2" },
+  { value: "australia-southeast1", label: "australia-southeast1" },
+  { value: "europe-north1", label: "europe-north1" },
+  { value: "europe-west1", label: "europe-west1" },
+  { value: "europe-west2", label: "europe-west2" },
+  { value: "europe-west3", label: "europe-west3" },
+  { value: "europe-west4", label: "europe-west4" },
+  { value: "europe-west6", label: "europe-west6" },
+  { value: "northamerica-northeast1", label: "northamerica-northeast1" },
+  { value: "southamerica-east1", label: "southamerica-east1" },
+  { value: "us-central1", label: "us-central1" },
+  { value: "us-east1", label: "us-east1" },
+  { value: "us-east4", label: "us-east4" },
+  { value: "us-west1", label: "us-west1" },
+  { value: "us-west2", label: "us-west2" },
+  { value: "us-west3", label: "us-west3" },
+  { value: "us-west4", label: "us-west4" },
+];

+ 3 - 1
dashboard/src/main/home/onboarding/state/StateHandler.ts

@@ -3,6 +3,7 @@ import type {
   AWSProvisionerConfig,
   AWSProvisionerConfig,
   AWSRegistryConfig,
   AWSRegistryConfig,
   DORegistryConfig,
   DORegistryConfig,
+  GARRegistryConfig,
   GCPProvisionerConfig,
   GCPProvisionerConfig,
   GCPRegistryConfig,
   GCPRegistryConfig,
   SkipProvisionConfig,
   SkipProvisionConfig,
@@ -12,6 +13,7 @@ import type {
 export type ConnectedRegistryConfig =
 export type ConnectedRegistryConfig =
   | AWSRegistryConfig
   | AWSRegistryConfig
   | GCPRegistryConfig
   | GCPRegistryConfig
+  | GARRegistryConfig
   | DORegistryConfig
   | DORegistryConfig
   | SkipRegistryConnection;
   | SkipRegistryConnection;
 
 
@@ -34,7 +36,7 @@ export type OnboardingState = {
   user_email: string;
   user_email: string;
   project: ProjectData | null;
   project: ProjectData | null;
   connected_source: ConnectedSourceData | null;
   connected_source: ConnectedSourceData | null;
-  connected_registry: any | null;
+  connected_registry: ConnectedRegistryConfig | null;
   provision_resources: Partial<ProvisionerConfig> | null;
   provision_resources: Partial<ProvisionerConfig> | null;
   actions: {
   actions: {
     restoreState: (state: OnboardingState) => void;
     restoreState: (state: OnboardingState) => void;

+ 12 - 0
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx

@@ -23,6 +23,7 @@ import {
 
 
 import {
 import {
   CredentialsForm as GCPCredentialsForm,
   CredentialsForm as GCPCredentialsForm,
+  GARegistryConfig,
   SettingsForm as GCPSettingsForm,
   SettingsForm as GCPSettingsForm,
   TestRegistryConnection as GCPTestRegistryConnection,
   TestRegistryConnection as GCPTestRegistryConnection,
 } from "./_GCPRegistryForm";
 } from "./_GCPRegistryForm";
@@ -42,6 +43,11 @@ const Forms = {
     settings: GCPSettingsForm,
     settings: GCPSettingsForm,
     test_connection: GCPTestRegistryConnection,
     test_connection: GCPTestRegistryConnection,
   },
   },
+  gar: {
+    credentials: GCPCredentialsForm,
+    settings: GARegistryConfig,
+    test_connection: GCPTestRegistryConnection,
+  },
   do: {
   do: {
     credentials: DOCredentialsForm,
     credentials: DOCredentialsForm,
     settings: DOSettingsForm,
     settings: DOSettingsForm,
@@ -62,6 +68,12 @@ const FormTitle = {
     doc:
     doc:
       "https://docs.porter.run/deploying-applications/deploying-from-docker-registry/linking-existing-registry#google-container-registry-gcr",
       "https://docs.porter.run/deploying-applications/deploying-from-docker-registry/linking-existing-registry#google-container-registry-gcr",
   },
   },
+  gar: {
+    label: "Google Artifact Registry (GAR)",
+    icon: integrationList["gcr"].icon,
+    doc:
+      "https://docs.porter.run/deploying-applications/deploying-from-docker-registry/linking-existing-registry#google-artifact-registry-gar",
+  },
   do: {
   do: {
     label: "DigitalOcean Container Registry (DOCR)",
     label: "DigitalOcean Container Registry (DOCR)",
     icon: integrationList["do"].icon,
     icon: integrationList["do"].icon,

+ 135 - 0
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx

@@ -1,9 +1,11 @@
 import Helper from "components/form-components/Helper";
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import InputRow from "components/form-components/InputRow";
+import SelectRow from "components/form-components/SelectRow";
 import UploadArea from "components/form-components/UploadArea";
 import UploadArea from "components/form-components/UploadArea";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
 import RegistryImageList from "main/home/onboarding/components/RegistryImageList";
+import { GCP_REGION_OPTIONS } from "main/home/onboarding/constants";
 import { OFState } from "main/home/onboarding/state";
 import { OFState } from "main/home/onboarding/state";
 import { StateHandler } from "main/home/onboarding/state/StateHandler";
 import { StateHandler } from "main/home/onboarding/state/StateHandler";
 import { GCPRegistryConfig } from "main/home/onboarding/types";
 import { GCPRegistryConfig } from "main/home/onboarding/types";
@@ -305,6 +307,139 @@ export const SettingsForm: React.FC<{
   );
   );
 };
 };
 
 
+export const GARegistryConfig: React.FC<{
+  nextFormStep: (data: Partial<GCPRegistryConfig>) => void;
+  project: any;
+}> = ({ nextFormStep, project }) => {
+  const [buttonStatus, setButtonStatus] = useState("");
+  const [registryName, setRegistryName] = useState("");
+  const [region, setRegion] = useState("us-east1");
+
+  const snap = useSnapshot(OFState);
+
+  const validate = () => {
+    if (!registryName) {
+      return {
+        hasError: true,
+        error: "Registry Name cannot be empty",
+      };
+    }
+    if (!region) {
+      return {
+        hasError: true,
+        error: "Region is missing",
+      };
+    }
+
+    if (!GCP_REGION_OPTIONS.map((val) => val.value).includes(region)) {
+      return {
+        hasError: true,
+        error: "Region is invalid",
+      };
+    }
+
+    return { hasError: false, error: "" };
+  };
+
+  const submit = async () => {
+    const validation = validate();
+
+    if (validation.hasError) {
+      setButtonStatus(validation.error);
+      return;
+    }
+
+    setButtonStatus("loading");
+
+    let gcpProjectId = NaN;
+
+    try {
+      const gcp_integration = await api
+        .getGCPIntegration("<token>", {}, { project_id: project.id })
+        .then((res) => {
+          let integrations = res.data;
+
+          let lastUsed = integrations.find((i: any) => {
+            return (
+              i.id === snap.StateHandler?.connected_registry?.credentials?.id
+            );
+          });
+          return lastUsed;
+        });
+
+      if (gcp_integration) {
+        gcpProjectId = gcp_integration.gpc_project_id;
+      }
+    } catch (error) {
+      setButtonStatus("Couldn't get the project id from the GCP integration.");
+      return;
+    }
+
+    const registryUrl = `${region}-docker.pkg.dev/${gcpProjectId}`;
+
+    try {
+      const data = await api
+        .connectGCRRegistry(
+          "<token>",
+          {
+            name: registryName,
+            gcp_integration_id:
+              snap.StateHandler.connected_registry.credentials.id,
+            url: registryUrl,
+          },
+          {
+            id: project.id,
+          }
+        )
+        .then((res) => res?.data);
+      nextFormStep({
+        settings: {
+          registry_connection_id: data.id,
+          gcr_url: registryUrl,
+          registry_name: registryName,
+        },
+      });
+    } catch (error) {
+      setButtonStatus("Couldn't connect registry.");
+    }
+  };
+  return (
+    <>
+      <Helper>Porter will use this registry to store your images.</Helper>
+      <InputRow
+        type="text"
+        value={registryName}
+        setValue={(name: string) => setRegistryName(name)}
+        isRequired={true}
+        label="🏷️ Registry Name"
+        placeholder="ex: paper-straw"
+        width="100%"
+      />
+      <SelectRow
+        options={GCP_REGION_OPTIONS}
+        width="100%"
+        value={region}
+        scrollBuffer={true}
+        dropdownMaxHeight="240px"
+        setActiveValue={(x: string) => {
+          setRegion(x);
+        }}
+        label="📍 GCP Region"
+      />
+      <Br />
+      <SaveButton
+        text="Continue"
+        disabled={false}
+        onClick={submit}
+        makeFlush={true}
+        clearPosition={true}
+        status={buttonStatus}
+        statusPosition={"right"}
+      />
+    </>
+  );
+};
+
 export const TestRegistryConnection: React.FC<{
 export const TestRegistryConnection: React.FC<{
   nextFormStep: () => void;
   nextFormStep: () => void;
   project: any;
   project: any;

+ 1 - 1
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx

@@ -203,7 +203,7 @@ const ProvisionResources: React.FC<{}> = () => {
       case "aws":
       case "aws":
         return ["eks", "ecr"];
         return ["eks", "ecr"];
       case "gcp":
       case "gcp":
-        return ["gke", "gcr"];
+        return ["gke", "gcr", "gar"];
       case "do":
       case "do":
         return ["doks", "docr"];
         return ["doks", "docr"];
     }
     }

+ 14 - 35
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx

@@ -4,6 +4,7 @@ import SelectRow from "components/form-components/SelectRow";
 import UploadArea from "components/form-components/UploadArea";
 import UploadArea from "components/form-components/UploadArea";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
+import { GCP_REGION_OPTIONS } from "main/home/onboarding/constants";
 import { OFState } from "main/home/onboarding/state";
 import { OFState } from "main/home/onboarding/state";
 import {
 import {
   GCPProvisionerConfig,
   GCPProvisionerConfig,
@@ -16,33 +17,6 @@ import { Infrastructure } from "shared/types";
 import styled from "styled-components";
 import styled from "styled-components";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
 
 
-const regionOptions = [
-  { value: "asia-east1", label: "asia-east1" },
-  { value: "asia-east2", label: "asia-east2" },
-  { value: "asia-northeast1", label: "asia-northeast1" },
-  { value: "asia-northeast2", label: "asia-northeast2" },
-  { value: "asia-northeast3", label: "asia-northeast3" },
-  { value: "asia-south1", label: "asia-south1" },
-  { value: "asia-southeast1", label: "asia-southeast1" },
-  { value: "asia-southeast2", label: "asia-southeast2" },
-  { value: "australia-southeast1", label: "australia-southeast1" },
-  { value: "europe-north1", label: "europe-north1" },
-  { value: "europe-west1", label: "europe-west1" },
-  { value: "europe-west2", label: "europe-west2" },
-  { value: "europe-west3", label: "europe-west3" },
-  { value: "europe-west4", label: "europe-west4" },
-  { value: "europe-west6", label: "europe-west6" },
-  { value: "northamerica-northeast1", label: "northamerica-northeast1" },
-  { value: "southamerica-east1", label: "southamerica-east1" },
-  { value: "us-central1", label: "us-central1" },
-  { value: "us-east1", label: "us-east1" },
-  { value: "us-east4", label: "us-east4" },
-  { value: "us-west1", label: "us-west1" },
-  { value: "us-west2", label: "us-west2" },
-  { value: "us-west3", label: "us-west3" },
-  { value: "us-west4", label: "us-west4" },
-];
-
 export const CredentialsForm: React.FC<{
 export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<GCPRegistryConfig>) => void;
   nextFormStep: (data: Partial<GCPRegistryConfig>) => void;
   project: any;
   project: any;
@@ -275,7 +249,7 @@ export const SettingsForm: React.FC<{
           .filter((infra) => infra.kind == "gke")
           .filter((infra) => infra.kind == "gke")
           .sort(sortFunc);
           .sort(sortFunc);
         const matchedGCRInfras = data
         const matchedGCRInfras = data
-          .filter((infra) => infra.kind == "gcr")
+          .filter((infra) => infra.kind == "gcr" || infra.kind == "gar")
           .sort(sortFunc);
           .sort(sortFunc);
 
 
         if (matchedGKEInfras.length > 0) {
         if (matchedGKEInfras.length > 0) {
@@ -327,7 +301,8 @@ export const SettingsForm: React.FC<{
     infras: { kind: string; status: string }[]
     infras: { kind: string; status: string }[]
   ) => {
   ) => {
     return !!infras.find(
     return !!infras.find(
-      (i) => ["docr", "gcr", "ecr"].includes(i.kind) && i.status === "created"
+      (i) =>
+        ["docr", "gcr", "ecr", "gar"].includes(i.kind) && i.status === "created"
     );
     );
   };
   };
 
 
@@ -370,7 +345,7 @@ export const SettingsForm: React.FC<{
     let clusterProvisionResponse = null;
     let clusterProvisionResponse = null;
     if (snap.StateHandler.connected_registry.skip) {
     if (snap.StateHandler.connected_registry.skip) {
       if (!hasRegistryProvisioned(infras)) {
       if (!hasRegistryProvisioned(infras)) {
-        registryProvisionResponse = await provisionGCR(integrationId);
+        registryProvisionResponse = await provisionGAR(integrationId);
       }
       }
     }
     }
     if (!hasClusterProvisioned(infras)) {
     if (!hasClusterProvisioned(infras)) {
@@ -387,7 +362,7 @@ export const SettingsForm: React.FC<{
     });
     });
   };
   };
 
 
-  const provisionGCR = async (id: number) => {
+  const provisionGAR = async (id: number) => {
     // console.log("Provisioning GCR");
     // console.log("Provisioning GCR");
 
 
     // See if there's an infra for GKE that is in an errored state and the last operation
     // See if there's an infra for GKE that is in an errored state and the last operation
@@ -401,7 +376,9 @@ export const SettingsForm: React.FC<{
           "<token>",
           "<token>",
           {
           {
             gcp_integration_id: id,
             gcp_integration_id: id,
-            values: {},
+            values: {
+              gcp_region: region,
+            },
           },
           },
           { project_id: project.id, infra_id: currGCRInfra.id }
           { project_id: project.id, infra_id: currGCRInfra.id }
         );
         );
@@ -414,9 +391,11 @@ export const SettingsForm: React.FC<{
         const res = await api.provisionInfra(
         const res = await api.provisionInfra(
           "<token>",
           "<token>",
           {
           {
-            kind: "gcr",
+            kind: "gar",
             gcp_integration_id: id,
             gcp_integration_id: id,
-            values: {},
+            values: {
+              gcp_region: region,
+            },
           },
           },
           { project_id: project.id }
           { project_id: project.id }
         );
         );
@@ -489,7 +468,7 @@ export const SettingsForm: React.FC<{
         isRequired={true}
         isRequired={true}
       />
       />
       <SelectRow
       <SelectRow
-        options={regionOptions}
+        options={GCP_REGION_OPTIONS}
         width="100%"
         width="100%"
         value={region}
         value={region}
         scrollBuffer={true}
         scrollBuffer={true}

+ 13 - 0
dashboard/src/main/home/onboarding/types.ts

@@ -34,6 +34,19 @@ export type GCPRegistryConfig = {
   };
   };
 };
 };
 
 
+export type GARRegistryConfig = {
+  skip: false;
+  provider: "gar";
+  credentials: {
+    id: number;
+  };
+  settings: {
+    registry_connection_id: number;
+    registry_name: string;
+    gar_url: string;
+  };
+};
+
 export type DORegistryConfig = {
 export type DORegistryConfig = {
   skip: false;
   skip: false;
   provider: "do";
   provider: "do";

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

@@ -2003,6 +2003,22 @@ const createStack = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks`
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks`
 );
 );
 
 
+const updateStack = baseApi<
+  {
+    name: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    namespace: string;
+    stack_id: string;
+  }
+>(
+  "PATCH",
+  ({ project_id, cluster_id, namespace, stack_id }) =>
+    `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}`
+);
+
 const listStacks = baseApi<
 const listStacks = baseApi<
   {},
   {},
   { project_id: number; cluster_id: number; namespace: string }
   { project_id: number; cluster_id: number; namespace: string }
@@ -2339,6 +2355,7 @@ export default {
   getStack,
   getStack,
   getStackRevision,
   getStackRevision,
   createStack,
   createStack,
+  updateStack,
   rollbackStack,
   rollbackStack,
   deleteStack,
   deleteStack,
   updateStackSourceConfig,
   updateStackSourceConfig,

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

@@ -64,6 +64,11 @@ export const integrationList: any = {
       "https://carlossanchez.files.wordpress.com/2019/06/21046548.png?w=640",
       "https://carlossanchez.files.wordpress.com/2019/06/21046548.png?w=640",
     label: "Google Container Registry (GCR)",
     label: "Google Container Registry (GCR)",
   },
   },
+  gar: {
+    icon:
+      "https://carlossanchez.files.wordpress.com/2019/06/21046548.png?w=640",
+    label: "Google Artifact Registry (GAR)",
+  },
   ecr: {
   ecr: {
     icon:
     icon:
       "https://avatars2.githubusercontent.com/u/52505464?s=400&u=da920f994c67665c7ad6c606a5286557d4f8555f&v=4",
       "https://avatars2.githubusercontent.com/u/52505464?s=400&u=da920f994c67665c7ad6c606a5286557d4f8555f&v=4",
@@ -97,6 +102,11 @@ export const integrationList: any = {
     icon: gcp,
     icon: gcp,
     label: "GCP",
     label: "GCP",
   },
   },
+  gar: {
+    icon:
+      "https://carlossanchez.files.wordpress.com/2019/06/21046548.png?w=640",
+    label: "Google Artifact Registry (GAR)",
+  },
   do: {
   do: {
     icon: digitalOcean,
     icon: digitalOcean,
     label: "DigitalOcean",
     label: "DigitalOcean",

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

@@ -410,6 +410,7 @@ export type InfraKind =
   | "s3"
   | "s3"
   | "gke"
   | "gke"
   | "gcr"
   | "gcr"
+  | "gar"
   | "doks"
   | "doks"
   | "docr"
   | "docr"
   | "aks"
   | "aks"
@@ -528,6 +529,13 @@ export const KindMap: ProviderInfoMap = {
     resource_link: "/integrations/registry",
     resource_link: "/integrations/registry",
     provider_name: "Google Container Registry (GCR)",
     provider_name: "Google Container Registry (GCR)",
   },
   },
+  gar: {
+    provider: "gcp",
+    source: "porter/gcp/gar",
+    resource_name: "Registry",
+    resource_link: "/integrations/registry",
+    provider_name: "Google Artifact Registry (GAR)",
+  },
   gke: {
   gke: {
     provider: "gcp",
     provider: "gcp",
     source: "porter/gcp/gke",
     source: "porter/gcp/gke",

+ 7 - 3
go.mod

@@ -3,7 +3,7 @@ module github.com/porter-dev/porter
 go 1.18
 go 1.18
 
 
 require (
 require (
-	cloud.google.com/go v0.99.0
+	cloud.google.com/go v0.102.0
 	github.com/AlecAivazis/survey/v2 v2.2.9
 	github.com/AlecAivazis/survey/v2 v2.2.9
 	github.com/Masterminds/semver/v3 v3.1.1
 	github.com/Masterminds/semver/v3 v3.1.1
 	github.com/aws/aws-sdk-go v1.43.28
 	github.com/aws/aws-sdk-go v1.43.28
@@ -49,7 +49,7 @@ require (
 	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
 	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
 	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
 	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
 	golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26
 	golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26
-	google.golang.org/api v0.62.0
+	google.golang.org/api v0.88.0
 	google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03
 	google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03
 	google.golang.org/grpc v1.47.0
 	google.golang.org/grpc v1.47.0
 	google.golang.org/protobuf v1.28.0
 	google.golang.org/protobuf v1.28.0
@@ -74,6 +74,9 @@ require (
 )
 )
 
 
 require (
 require (
+	cloud.google.com/go/artifactregistry v1.3.0 // indirect
+	cloud.google.com/go/compute v1.7.0 // indirect
+	cloud.google.com/go/iam v0.3.0 // indirect
 	github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect
 	github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
@@ -104,6 +107,7 @@ require (
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
 	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
 	github.com/google/gnostic v0.6.9 // indirect
 	github.com/google/gnostic v0.6.9 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
@@ -182,7 +186,7 @@ require (
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/google/uuid v1.3.0 // indirect
-	github.com/googleapis/gax-go/v2 v2.1.1 // indirect
+	github.com/googleapis/gax-go/v2 v2.4.0 // indirect
 	github.com/googleapis/gnostic v0.5.5 // indirect
 	github.com/googleapis/gnostic v0.5.5 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/gosuri/uitable v0.0.4 // indirect
 	github.com/gosuri/uitable v0.0.4 // indirect

+ 78 - 0
go.sum

@@ -33,17 +33,33 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud
 cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
 cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
 cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY=
 cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY=
 cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
 cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
+cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=
+cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
+cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8=
+cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
+cloud.google.com/go/artifactregistry v1.3.0 h1:kB+76CLiFcliaoEG51lxvvDvF9GkjnN0YrF8kZDh+/Q=
+cloud.google.com/go/artifactregistry v1.3.0/go.mod h1:plM9tUGHmFSJuzbaLena6C4v4QoGpRQJkXqo3W3ajYw=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
+cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
+cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
+cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
+cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc=
+cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
+cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk=
+cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
 cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU=
 cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU=
 cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
 cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
+cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
+cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -55,6 +71,7 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
 contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
 contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
 github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
@@ -1023,16 +1040,25 @@ github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
+github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw=
+github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
 github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
 github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
 github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
 github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
 github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
+github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
+github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI=
+github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
+github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
+github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
 github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
 github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
 github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
 github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
 github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
+github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
 github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
 github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
 github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -2282,10 +2308,13 @@ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
 golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
 golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -2308,8 +2337,11 @@ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
+golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
 golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26 h1:uBgVQYJLi/m8M0wzp+aGwBWt90gMRoOVf+aWTW10QHI=
 golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26 h1:uBgVQYJLi/m8M0wzp+aGwBWt90gMRoOVf+aWTW10QHI=
 golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
 golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2464,15 +2496,23 @@ golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
 golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
 golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -2638,6 +2678,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
 gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
 gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
 gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
@@ -2677,6 +2720,19 @@ google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUb
 google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
 google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
 google.golang.org/api v0.62.0 h1:PhGymJMXfGBzc4lBRmrx9+1w4w2wEzURHNGF/sD/xGc=
 google.golang.org/api v0.62.0 h1:PhGymJMXfGBzc4lBRmrx9+1w4w2wEzURHNGF/sD/xGc=
 google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
 google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
+google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
+google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
+google.golang.org/api v0.70.0 h1:67zQnAE0T2rB0A3CwLSas0K+SbVzSxP+zTLkQLexeiw=
+google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
+google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
+google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
+google.golang.org/api v0.75.0 h1:0AYh/ae6l9TDUvIQrDw5QRpM100P6oHgD+o3dYHMzJg=
+google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
+google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
+google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
+google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
+google.golang.org/api v0.88.0 h1:MPwxQRqpyskYhr2iNyfsQ8R06eeyhe7UEuR30p136ZQ=
+google.golang.org/api v0.88.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2738,6 +2794,7 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
@@ -2763,9 +2820,28 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
+google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 h1:nquqdM9+ps0JZcIiI70+tqoaIFS5Ql4ZuK8UXnz3HfE=
 google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 h1:nquqdM9+ps0JZcIiI70+tqoaIFS5Ql4ZuK8UXnz3HfE=
 google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
+google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
+google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03 h1:W70HjnmXFJm+8RNjOpIDYW2nKsSi/af0VvIZUtYkwuU=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03 h1:W70HjnmXFJm+8RNjOpIDYW2nKsSi/af0VvIZUtYkwuU=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
@@ -2802,9 +2878,11 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
 google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
 google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
 google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
 google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
 google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=

+ 2 - 0
internal/helm/postrenderer.go

@@ -813,6 +813,8 @@ func getRegNameFromImageRef(image string) (string, error) {
 	// if registry is dockerhub, leave the image name as-is
 	// if registry is dockerhub, leave the image name as-is
 	if strings.Contains(domain, "docker.io") {
 	if strings.Contains(domain, "docker.io") {
 		regName = "index.docker.io/" + path
 		regName = "index.docker.io/" + path
+	} else if strings.Contains(domain, "pkg.dev") {
+		regName = domain + "/" + strings.Split(path, "/")[0]
 	} else {
 	} else {
 		regName = domain
 		regName = domain
 
 

+ 3 - 1
internal/kubernetes/prometheus/metrics.go

@@ -301,7 +301,7 @@ func getSelectionRegex(kind, name string) (string, error) {
 
 
 	switch strings.ToLower(kind) {
 	switch strings.ToLower(kind) {
 	case "deployment":
 	case "deployment":
-		suffix = "[a-z0-9]+-[a-z0-9]+"
+		suffix = "[a-z0-9]+"
 	case "statefulset":
 	case "statefulset":
 		suffix = "[0-9]+"
 		suffix = "[0-9]+"
 	case "job":
 	case "job":
@@ -310,6 +310,8 @@ func getSelectionRegex(kind, name string) (string, error) {
 		suffix = "[a-z0-9]+-[a-z0-9]+"
 		suffix = "[a-z0-9]+-[a-z0-9]+"
 	case "ingress":
 	case "ingress":
 		return name, nil
 		return name, nil
+	case "daemonset":
+		suffix = "[a-z0-9]+"
 	default:
 	default:
 		return "", fmt.Errorf("not a supported controller to query for metrics")
 		return "", fmt.Errorf("not a supported controller to query for metrics")
 	}
 	}

+ 5 - 1
internal/models/registry.go

@@ -45,7 +45,11 @@ func (r *Registry) ToRegistryType() *types.Registry {
 	if r.AWSIntegrationID != 0 {
 	if r.AWSIntegrationID != 0 {
 		serv = types.ECR
 		serv = types.ECR
 	} else if r.GCPIntegrationID != 0 {
 	} else if r.GCPIntegrationID != 0 {
-		serv = types.GCR
+		if strings.Contains(r.URL, "pkg.dev") {
+			serv = types.GAR
+		} else {
+			serv = types.GCR
+		}
 	} else if r.DOIntegrationID != 0 {
 	} else if r.DOIntegrationID != 0 {
 		serv = types.DOCR
 		serv = types.DOCR
 	} else if r.AzureIntegrationID != 0 {
 	} else if r.AzureIntegrationID != 0 {

+ 3 - 0
internal/models/stack.go

@@ -164,6 +164,8 @@ type StackSourceConfig struct {
 
 
 	Name string
 	Name string
 
 
+	DisplayName string
+
 	UID string
 	UID string
 
 
 	ImageRepoURI string
 	ImageRepoURI string
@@ -183,6 +185,7 @@ func (s StackSourceConfig) ToStackSourceConfigType(stackID string, stackRevision
 		ID:              s.UID,
 		ID:              s.UID,
 		ImageRepoURI:    s.ImageRepoURI,
 		ImageRepoURI:    s.ImageRepoURI,
 		ImageTag:        s.ImageTag,
 		ImageTag:        s.ImageTag,
+		DisplayName:     s.DisplayName,
 	}
 	}
 }
 }
 
 

+ 232 - 1
internal/registry/registry.go

@@ -11,6 +11,7 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
+	artifactregistry "cloud.google.com/go/artifactregistry/apiv1beta2"
 	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
 	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/service/ecr"
 	"github.com/aws/aws-sdk-go/service/ecr"
@@ -18,6 +19,10 @@ import (
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2"
+	v1artifactregistry "google.golang.org/api/artifactregistry/v1"
+	"google.golang.org/api/iterator"
+	"google.golang.org/api/option"
+	artifactregistrypb "google.golang.org/genproto/googleapis/devtools/artifactregistry/v1beta2"
 
 
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 
 
@@ -70,7 +75,11 @@ func (r *Registry) ListRepositories(
 	}
 	}
 
 
 	if r.GCPIntegrationID != 0 {
 	if r.GCPIntegrationID != 0 {
-		return r.listGCRRepositories(repo)
+		if strings.Contains(r.URL, "pkg.dev") {
+			return r.listGARRepositories(repo)
+		} else {
+			return r.listGCRRepositories(repo)
+		}
 	}
 	}
 
 
 	if r.DOIntegrationID != 0 {
 	if r.DOIntegrationID != 0 {
@@ -203,6 +212,103 @@ func (r *Registry) listGCRRepositories(
 	return res, nil
 	return res, nil
 }
 }
 
 
+func (r *Registry) GetGARToken(repo repository.Repository) (*oauth2.Token, error) {
+	getTokenCache := r.getTokenCacheFunc(repo)
+
+	gcp, err := repo.GCPIntegration().ReadGCPIntegration(
+		r.ProjectID,
+		r.GCPIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// get oauth2 access token
+	return gcp.GetBearerToken(
+		getTokenCache,
+		r.setTokenCacheFunc(repo),
+		"https://www.googleapis.com/auth/cloud-platform",
+	)
+}
+
+type garTokenSource struct {
+	reg  *Registry
+	repo repository.Repository
+}
+
+func (source *garTokenSource) Token() (*oauth2.Token, error) {
+	return source.reg.GetGARToken(source.repo)
+}
+
+func (r *Registry) listGARRepositories(
+	repo repository.Repository,
+) ([]*ptypes.RegistryRepository, error) {
+	gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
+		r.ProjectID,
+		r.GCPIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := artifactregistry.NewClient(context.Background(), option.WithTokenSource(&garTokenSource{
+		reg:  r,
+		repo: repo,
+	}), option.WithScopes("roles/artifactregistry.reader"))
+
+	if err != nil {
+		return nil, err
+	}
+
+	var res []*ptypes.RegistryRepository
+	nextToken := ""
+
+	parsedURL, err := url.Parse("https://" + r.URL)
+
+	if err != nil {
+		return nil, err
+	}
+
+	location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
+
+	for {
+		it := client.ListRepositories(context.Background(), &artifactregistrypb.ListRepositoriesRequest{
+			Parent:    fmt.Sprintf("projects/%s/locations/%s", gcpInt.GCPProjectID, location),
+			PageSize:  1000,
+			PageToken: nextToken,
+		})
+
+		for {
+			resp, err := it.Next()
+
+			if err == iterator.Done {
+				break
+			} else if err != nil {
+				return nil, err
+			}
+
+			repoSlice := strings.Split(resp.GetName(), "/")
+			repoName := repoSlice[len(repoSlice)-1]
+
+			res = append(res, &ptypes.RegistryRepository{
+				Name:      resp.GetName(),
+				CreatedAt: resp.GetCreateTime().AsTime(),
+				URI:       parsedURL.Host + "/" + gcpInt.GCPProjectID + "/" + repoName,
+			})
+		}
+
+		if it.PageInfo().Token == "" {
+			break
+		}
+
+		nextToken = it.PageInfo().Token
+	}
+
+	return res, nil
+}
+
 func (r *Registry) listECRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
 func (r *Registry) listECRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
 	aws, err := repo.AWSIntegration().ReadAWSIntegration(
 	aws, err := repo.AWSIntegration().ReadAWSIntegration(
 		r.ProjectID,
 		r.ProjectID,
@@ -589,6 +695,8 @@ func (r *Registry) CreateRepository(
 	// if aws, create repository
 	// if aws, create repository
 	if r.AWSIntegrationID != 0 {
 	if r.AWSIntegrationID != 0 {
 		return r.createECRRepository(repo, name)
 		return r.createECRRepository(repo, name)
+	} else if r.GCPIntegrationID != 0 && strings.Contains(r.URL, "pkg.dev") {
+		return r.createGARRepository(repo, name)
 	}
 	}
 
 
 	// otherwise, no-op
 	// otherwise, no-op
@@ -635,6 +743,62 @@ func (r *Registry) createECRRepository(
 	return nil
 	return nil
 }
 }
 
 
+func (r *Registry) createGARRepository(
+	repo repository.Repository,
+	name string,
+) error {
+	gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
+		r.ProjectID,
+		r.GCPIntegrationID,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	client, err := artifactregistry.NewClient(context.Background(), option.WithTokenSource(&garTokenSource{
+		reg:  r,
+		repo: repo,
+	}), option.WithScopes("roles/artifactregistry.admin"))
+
+	if err != nil {
+		return err
+	}
+
+	defer client.Close()
+
+	parsedURL, err := url.Parse("https://" + r.URL)
+
+	if err != nil {
+		return err
+	}
+
+	location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
+
+	_, err = client.GetRepository(context.Background(), &artifactregistrypb.GetRepositoryRequest{
+		Name: fmt.Sprintf("projects/%s/locations/%s/repositories/%s", gcpInt.GCPProjectID, location, name),
+	})
+
+	if err != nil && strings.Contains(err.Error(), "not found") {
+		// create a new repository
+		_, err := client.CreateRepository(context.Background(), &artifactregistrypb.CreateRepositoryRequest{
+			Parent:       fmt.Sprintf("projects/%s/locations/%s", gcpInt.GCPProjectID, location),
+			RepositoryId: name,
+			Repository: &artifactregistrypb.Repository{
+				Format: artifactregistrypb.Repository_DOCKER,
+			},
+		})
+
+		if err != nil {
+			return err
+		}
+	} else if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // ListImages lists the images for an image repository
 // ListImages lists the images for an image repository
 func (r *Registry) ListImages(
 func (r *Registry) ListImages(
 	repoName string,
 	repoName string,
@@ -651,6 +815,10 @@ func (r *Registry) ListImages(
 	}
 	}
 
 
 	if r.GCPIntegrationID != 0 {
 	if r.GCPIntegrationID != 0 {
+		if strings.Contains(r.URL, "pkg.dev") {
+			return r.listGARImages(repoName, repo)
+		}
+
 		return r.listGCRImages(repoName, repo)
 		return r.listGCRImages(repoName, repo)
 	}
 	}
 
 
@@ -1003,6 +1171,69 @@ func (r *Registry) listGCRImages(repoName string, repo repository.Repository) ([
 	return res, nil
 	return res, nil
 }
 }
 
 
+func (r *Registry) listGARImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
+	gcpInt, err := repo.GCPIntegration().ReadGCPIntegration(
+		r.ProjectID,
+		r.GCPIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	svc, err := v1artifactregistry.NewService(context.Background(), option.WithTokenSource(&garTokenSource{
+		reg:  r,
+		repo: repo,
+	}), option.WithScopes("roles/artifactregistry.reader"))
+
+	if err != nil {
+		return nil, err
+	}
+
+	nextToken := ""
+	var res []*ptypes.Image
+
+	parsedURL, err := url.Parse("https://" + r.URL)
+
+	if err != nil {
+		return nil, err
+	}
+
+	location := strings.TrimSuffix(parsedURL.Host, "-docker.pkg.dev")
+
+	dockerSvc := v1artifactregistry.NewProjectsLocationsRepositoriesDockerImagesService(svc)
+
+	for {
+		resp, err := dockerSvc.List(fmt.Sprintf("projects/%s/locations/%s/repositories/%s",
+			gcpInt.GCPProjectID, location, repoName)).PageSize(1000).PageToken(nextToken).Do()
+
+		if err != nil {
+			return nil, err
+		}
+
+		for _, image := range resp.DockerImages {
+			uploadTime, _ := time.Parse(time.RFC3339, image.UploadTime)
+
+			for _, tag := range image.Tags {
+				res = append(res, &ptypes.Image{
+					RepositoryName: repoName,
+					Tag:            tag,
+					PushedAt:       &uploadTime,
+					Digest:         strings.Split(image.Name, "@")[1],
+				})
+			}
+		}
+
+		if resp.NextPageToken == "" {
+			break
+		}
+
+		nextToken = resp.NextPageToken
+	}
+
+	return res, nil
+}
+
 func (r *Registry) listDOCRImages(
 func (r *Registry) listDOCRImages(
 	repoName string,
 	repoName string,
 	repo repository.Repository,
 	repo repository.Repository,

+ 8 - 0
internal/repository/gorm/stack.go

@@ -118,6 +118,14 @@ func (repo *StackRepository) DeleteStack(stack *models.Stack) (*models.Stack, er
 	return stack, nil
 	return stack, nil
 }
 }
 
 
+func (repo *StackRepository) UpdateStack(stack *models.Stack) (*models.Stack, error) {
+	if err := repo.db.Save(stack).Error; err != nil {
+		return nil, err
+	}
+
+	return stack, nil
+}
+
 func (repo *StackRepository) UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error) {
 func (repo *StackRepository) UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error) {
 	if err := repo.db.Save(revision).Error; err != nil {
 	if err := repo.db.Save(revision).Error; err != nil {
 		return nil, err
 		return nil, err

+ 1 - 0
internal/repository/stack.go

@@ -9,6 +9,7 @@ type StackRepository interface {
 	ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error)
 	ReadStackByStringID(projectID uint, stackID string) (*models.Stack, error)
 	ListStacks(projectID uint, clusterID uint, namespace string) ([]*models.Stack, error)
 	ListStacks(projectID uint, clusterID uint, namespace string) ([]*models.Stack, error)
 	DeleteStack(stack *models.Stack) (*models.Stack, error)
 	DeleteStack(stack *models.Stack) (*models.Stack, error)
+	UpdateStack(stack *models.Stack) (*models.Stack, error)
 
 
 	UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error)
 	UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error)
 	ReadStackRevision(stackRevisionID uint) (*models.StackRevision, error)
 	ReadStackRevision(stackRevisionID uint) (*models.StackRevision, error)

+ 4 - 0
internal/repository/test/stack.go

@@ -35,6 +35,10 @@ func (repo *StackRepository) DeleteStack(stack *models.Stack) (*models.Stack, er
 	panic("unimplemented")
 	panic("unimplemented")
 }
 }
 
 
+func (repo *StackRepository) UpdateStack(stack *models.Stack) (*models.Stack, error) {
+	panic("unimplemented")
+}
+
 func (repo *StackRepository) UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error) {
 func (repo *StackRepository) UpdateStackRevision(revision *models.StackRevision) (*models.StackRevision, error) {
 	panic("unimplemented")
 	panic("unimplemented")
 }
 }

+ 1 - 0
internal/stacks/helpers.go

@@ -22,6 +22,7 @@ func CloneSourceConfigs(sourceConfigs []models.StackSourceConfig) ([]models.Stac
 		res = append(res, models.StackSourceConfig{
 		res = append(res, models.StackSourceConfig{
 			UID:          uid,
 			UID:          uid,
 			Name:         sourceConfig.Name,
 			Name:         sourceConfig.Name,
+			DisplayName:  sourceConfig.DisplayName,
 			ImageRepoURI: sourceConfig.ImageRepoURI,
 			ImageRepoURI: sourceConfig.ImageRepoURI,
 			ImageTag:     sourceConfig.ImageTag,
 			ImageTag:     sourceConfig.ImageTag,
 		})
 		})

+ 1 - 1
provisioner/server/handlers/provision/apply.go

@@ -153,7 +153,7 @@ func (c *ProvisionApplyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 				InfraID:                infra.ID,
 				InfraID:                infra.ID,
 			},
 			},
 		))
 		))
-	case types.InfraDOCR, types.InfraECR, types.InfraGCR, types.InfraACR:
+	case types.InfraDOCR, types.InfraECR, types.InfraGCR, types.InfraGAR, types.InfraACR:
 		c.Config.AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
 		c.Config.AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
 			&analytics.RegistryProvisioningStartTrackOpts{
 			&analytics.RegistryProvisioningStartTrackOpts{
 				ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(0, infra.ProjectID),
 				ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(0, infra.ProjectID),

+ 17 - 2
provisioner/server/handlers/state/create_resource.go

@@ -110,6 +110,8 @@ func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		_, err = createDOCRRegistry(c.Config, infra, operation, req.Output)
 		_, err = createDOCRRegistry(c.Config, infra, operation, req.Output)
 	case string(types.InfraGCR):
 	case string(types.InfraGCR):
 		_, err = createGCRRegistry(c.Config, infra, operation, req.Output)
 		_, err = createGCRRegistry(c.Config, infra, operation, req.Output)
+	case string(types.InfraGAR):
+		_, err = createGARRegistry(c.Config, infra, operation, req.Output)
 	case string(types.InfraACR):
 	case string(types.InfraACR):
 		_, err = createACRRegistry(c.Config, infra, operation, req.Output)
 		_, err = createACRRegistry(c.Config, infra, operation, req.Output)
 	}
 	}
@@ -301,8 +303,9 @@ func createCluster(config *config.Config, infra *models.Infra, operation *models
 
 
 func getNewCluster(infra *models.Infra) *models.Cluster {
 func getNewCluster(infra *models.Infra) *models.Cluster {
 	res := &models.Cluster{
 	res := &models.Cluster{
-		ProjectID: infra.ProjectID,
-		InfraID:   infra.ID,
+		ProjectID:           infra.ProjectID,
+		InfraID:             infra.ID,
+		MonitorHelmReleases: true,
 	}
 	}
 
 
 	switch infra.Kind {
 	switch infra.Kind {
@@ -367,6 +370,18 @@ func createGCRRegistry(config *config.Config, infra *models.Infra, operation *mo
 	return config.Repo.Registry().CreateRegistry(reg)
 	return config.Repo.Registry().CreateRegistry(reg)
 }
 }
 
 
+func createGARRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
+	reg := &models.Registry{
+		ProjectID:        infra.ProjectID,
+		GCPIntegrationID: infra.GCPIntegrationID,
+		InfraID:          infra.ID,
+		URL:              output["url"].(string),
+		Name:             "gar-registry",
+	}
+
+	return config.Repo.Registry().CreateRegistry(reg)
+}
+
 func createACRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
 func createACRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
 	reg := &models.Registry{
 	reg := &models.Registry{
 		ProjectID:          infra.ProjectID,
 		ProjectID:          infra.ProjectID,

+ 1 - 1
provisioner/server/handlers/state/delete_resource.go

@@ -69,7 +69,7 @@ func (c *DeleteResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 
 	// switch on the kind of resource and write the corresponding objects to the database
 	// switch on the kind of resource and write the corresponding objects to the database
 	switch infra.Kind {
 	switch infra.Kind {
-	case types.InfraECR, types.InfraGCR, types.InfraDOCR, types.InfraACR:
+	case types.InfraECR, types.InfraGCR, types.InfraGAR, types.InfraDOCR, types.InfraACR:
 		_, err = deleteRegistry(c.Config, infra, operation)
 		_, err = deleteRegistry(c.Config, infra, operation)
 	case types.InfraEKS, types.InfraDOKS, types.InfraGKE, types.InfraAKS:
 	case types.InfraEKS, types.InfraDOKS, types.InfraGKE, types.InfraAKS:
 		_, err = deleteCluster(c.Config, infra, operation)
 		_, err = deleteCluster(c.Config, infra, operation)

+ 10 - 10
workers/utils/retry_helm_agent.go

@@ -4,8 +4,8 @@ package utils
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"log"
 	"os"
 	"os"
-	"strings"
 	"time"
 	"time"
 
 
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
@@ -52,14 +52,14 @@ func (a *RetryHelmAgent) ListReleases(
 
 
 		if err == nil {
 		if err == nil {
 			return releases, nil
 			return releases, nil
-		} else if strings.Contains(err.Error(), "Unauthorized") {
+		} else {
+			log.Printf("recreating helm agent for retrying ListReleases. Error: %v", err)
+
 			a.agent, err = helm.GetAgentOutOfClusterConfig(a.form, a.l)
 			a.agent, err = helm.GetAgentOutOfClusterConfig(a.form, a.l)
 
 
 			if err != nil {
 			if err != nil {
 				return nil, fmt.Errorf("error recreating helm agent for retrying ListReleases: %w", err)
 				return nil, fmt.Errorf("error recreating helm agent for retrying ListReleases: %w", err)
 			}
 			}
-		} else {
-			return nil, err
 		}
 		}
 
 
 		time.Sleep(a.retryInterval)
 		time.Sleep(a.retryInterval)
@@ -76,14 +76,14 @@ func (a *RetryHelmAgent) GetReleaseHistory(
 
 
 		if err == nil {
 		if err == nil {
 			return releases, nil
 			return releases, nil
-		} else if strings.Contains(err.Error(), "Unauthorized") {
+		} else {
+			log.Printf("recreating helm agent for retrying GetReleaseHistory. Error: %v", err)
+
 			a.agent, err = helm.GetAgentOutOfClusterConfig(a.form, a.l)
 			a.agent, err = helm.GetAgentOutOfClusterConfig(a.form, a.l)
 
 
 			if err != nil {
 			if err != nil {
 				return nil, fmt.Errorf("error recreating helm agent for retrying GetReleaseHistory: %w", err)
 				return nil, fmt.Errorf("error recreating helm agent for retrying GetReleaseHistory: %w", err)
 			}
 			}
-		} else {
-			return nil, err
 		}
 		}
 
 
 		time.Sleep(a.retryInterval)
 		time.Sleep(a.retryInterval)
@@ -101,14 +101,14 @@ func (a *RetryHelmAgent) DeleteReleaseRevision(
 
 
 		if err == nil {
 		if err == nil {
 			return nil
 			return nil
-		} else if strings.Contains(err.Error(), "Unauthorized") {
+		} else {
+			log.Printf("recreating helm agent for retrying DeleteReleaseRevision. Error: %v", err)
+
 			a.agent, err = helm.GetAgentOutOfClusterConfig(a.form, a.l)
 			a.agent, err = helm.GetAgentOutOfClusterConfig(a.form, a.l)
 
 
 			if err != nil {
 			if err != nil {
 				return fmt.Errorf("error recreating helm agent for retrying DeleteReleaseRevision: %w", err)
 				return fmt.Errorf("error recreating helm agent for retrying DeleteReleaseRevision: %w", err)
 			}
 			}
-		} else {
-			return err
 		}
 		}
 
 
 		time.Sleep(a.retryInterval)
 		time.Sleep(a.retryInterval)