ソースを参照

Merge pull request #2285 from porter-dev/nafees/gcp-gar

[POR-648] Google Artifact Registry support
abelanger5 3 年 前
コミット
889c1bbed0
36 ファイル変更1144 行追加67 行削除
  1. 20 0
      api/client/registry.go
  2. 2 0
      api/server/handlers/infra/create.go
  3. 105 18
      api/server/handlers/infra/forms.go
  4. 2 0
      api/server/handlers/infra/get_template.go
  5. 8 0
      api/server/handlers/infra/list_templates.go
  6. 1 1
      api/server/handlers/registry/create.go
  7. 63 0
      api/server/handlers/registry/get_token.go
  8. 28 0
      api/server/router/project.go
  9. 1 0
      api/types/infra.go
  10. 5 0
      api/types/integrations.go
  11. 7 2
      api/types/registry.go
  12. 26 0
      cli/cmd/connect.go
  13. 92 0
      cli/cmd/connect/gar.go
  14. 2 2
      cli/cmd/connect/gcr.go
  15. 5 0
      cli/cmd/deploy/create.go
  16. 50 0
      cli/cmd/docker/auth.go
  17. 3 0
      dashboard/src/main/home/integrations/create-integration/CreateIntegrationForm.tsx
  18. 156 0
      dashboard/src/main/home/integrations/create-integration/GARForm.tsx
  19. 6 0
      dashboard/src/main/home/onboarding/components/ProviderSelector.tsx
  20. 26 0
      dashboard/src/main/home/onboarding/constants.ts
  21. 3 1
      dashboard/src/main/home/onboarding/state/StateHandler.ts
  22. 12 0
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx
  23. 135 0
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx
  24. 1 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx
  25. 14 35
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx
  26. 13 0
      dashboard/src/main/home/onboarding/types.ts
  27. 10 0
      dashboard/src/shared/common.tsx
  28. 8 0
      dashboard/src/shared/types.tsx
  29. 7 3
      go.mod
  30. 78 0
      go.sum
  31. 2 0
      internal/helm/postrenderer.go
  32. 5 1
      internal/models/registry.go
  33. 232 1
      internal/registry/registry.go
  34. 1 1
      provisioner/server/handlers/provision/apply.go
  35. 14 0
      provisioner/server/handlers/state/create_resource.go
  36. 1 1
      provisioner/server/handlers/state/delete_resource.go

+ 20 - 0
api/client/registry.go

@@ -123,6 +123,26 @@ func (c *Client) GetGCRAuthorizationToken(
 	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
 func (c *Client) GetACRAuthorizationToken(
 	ctx context.Context,

+ 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"
 	case types.InfraGCR:
 		return "porter/gcp/gcr", "v0.1.0"
+	case types.InfraGAR:
+		return "porter/gcp/gar", "v0.1.0"
 	case types.InfraGKE:
 		return "porter/gcp/gke", "v0.1.0"
 	case types.InfraDOCR:

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

@@ -9,7 +9,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: String to echo
     - type: string-input
@@ -27,7 +27,7 @@ tabs:
   label: Main
   sections:
   - name: heading
-    contents: 
+    contents:
     - type: heading
       label: S3 Settings
   - name: bucket_name
@@ -48,7 +48,7 @@ tabs:
   label: Main
   sections:
   - name: heading
-    contents: 
+    contents:
     - type: heading
       label: Database Settings
   - name: user
@@ -110,7 +110,7 @@ tabs:
         - label: "Postgres 13"
           value: postgres13
   - name: pg-9-versions
-    show_if: 
+    show_if:
       is: "postgres9"
       variable: db_family
     contents:
@@ -165,7 +165,7 @@ tabs:
         - label: "v9.6.23"
           value: "9.6.23"
   - name: pg-10-versions
-    show_if: 
+    show_if:
       is: "postgres10"
       variable: db_family
     contents:
@@ -212,7 +212,7 @@ tabs:
         - label: "v10.18"
           value: "10.18"
   - name: pg-11-versions
-    show_if: 
+    show_if:
       is: "postgres11"
       variable: db_family
     contents:
@@ -249,7 +249,7 @@ tabs:
         - label: "v11.13"
           value: "11.13"
   - name: pg-12-versions
-    show_if: 
+    show_if:
       is: "postgres12"
       variable: db_family
     contents:
@@ -276,7 +276,7 @@ tabs:
         - label: "v12.10"
           value: "12.10"
   - name: pg-13-versions
-    show_if: 
+    show_if:
       is: "postgres13"
       variable: db_family
     contents:
@@ -326,7 +326,7 @@ tabs:
         default: 20
     - type: checkbox
       variable: db_storage_encrypted
-      label: Enable storage encryption for the database. 
+      label: Enable storage encryption for the database.
       settings:
         default: false
 - name: advanced
@@ -353,7 +353,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: ECR Configuration
     - type: string-input
@@ -371,7 +371,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: EKS Configuration
     - type: select
@@ -513,7 +513,7 @@ tabs:
       settings:
         default: true
   - name: aws_auth_warning
-    show_if: 
+    show_if:
       not: manage_aws_auth_configmap
     contents:
     - type: subtitle
@@ -635,7 +635,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: GCR Configuration
     - type: select
@@ -698,6 +698,93 @@ tabs:
           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
 hasSource: false
 includeHiddenFields: true
@@ -706,7 +793,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: GKE Configuration
     - type: select
@@ -787,7 +874,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: DOCR Configuration
     - type: select
@@ -815,7 +902,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: DOKS Configuration
     - type: select
@@ -865,7 +952,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: ACR Configuration
     - type: select
@@ -900,7 +987,7 @@ tabs:
   label: Configuration
   sections:
   - name: section_one
-    contents: 
+    contents:
     - type: heading
       label: AKS Configuration
     - type: select

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

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

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

@@ -81,6 +81,14 @@ var templateMap = map[string]*types.InfraTemplateMeta{
 		Kind:               "gcr",
 		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": {
 		Icon:               "https://sysdig.com/wp-content/uploads/2016/08/GKE_color.png",
 		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
 
 	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 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)
 }
 
+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 {
 	handlers.PorterHandlerReadWriter
 }

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

@@ -608,6 +608,34 @@ func getProjectRoutes(
 		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
 	getACRTokenEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 1 - 0
api/types/infra.go

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

+ 5 - 0
api/types/integrations.go

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

+ 7 - 2
api/types/registry.go

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

+ 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{
 	Use:   "docr",
 	Short: "Adds a DOCR instance to a project",
@@ -127,6 +139,7 @@ func init() {
 	connectCmd.AddCommand(connectRegistryCmd)
 	connectCmd.AddCommand(connectDockerhubCmd)
 	connectCmd.AddCommand(connectGCRCmd)
+	connectCmd.AddCommand(connectGARCmd)
 	connectCmd.AddCommand(connectDOCRCmd)
 	connectCmd.AddCommand(connectHelmRepoCmd)
 }
@@ -179,6 +192,19 @@ func runConnectGCR(_ *types.GetAuthenticatedUserResponse, client *api.Client, _
 	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 {
 	regID, err := connect.DOCR(
 		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) {
 	// 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 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.
@@ -39,7 +39,7 @@ Key file location: `))
 			return 0, err
 		}
 
-		// create the aws integration
+		// create the gcp integration
 		integration, err := client.CreateGCPIntegration(
 			context.Background(),
 			projectID,

+ 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
 }
 

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

@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
+	"net/url"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -51,6 +52,8 @@ type AuthGetter struct {
 func (a *AuthGetter) GetCredentials(serverURL string) (user string, secret string, err error) {
 	if strings.Contains(serverURL, "gcr.io") {
 		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") {
 		return a.GetDOCRCredentials(serverURL, a.ProjectID)
 	} 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
 }
 
+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) {
 	cachedEntry := a.Cache.Get(serverURL)
 

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

@@ -8,6 +8,7 @@ import EKSForm from "./EKSForm";
 import GCRForm from "./GCRForm";
 import ECRForm from "./ECRForm";
 import GitlabForm from "./GitlabForm";
+import GARForm from "./GARForm";
 
 type PropsType = {
   integrationName: string;
@@ -34,6 +35,8 @@ export default class CreateIntegrationForm extends Component<
         return <ECRForm closeForm={this.props.closeForm} />;
       case "gcr":
         return <GCRForm closeForm={this.props.closeForm} />;
+      case "gar":
+        return <GARForm closeForm={this.props.closeForm} />;
       case "gitlab":
         return <GitlabForm closeForm={this.props.closeForm} />;
       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,
     label: "Google Cloud Registry (GCR)",
   },
+  {
+    value: "gar",
+    icon: integrationList["gcr"]?.icon,
+    label: "Google Artifact Registry (GAR)",
+  },
   {
     value: "do",
     icon: integrationList["do"]?.icon,
@@ -50,6 +55,7 @@ export const provisionerOptions = [
     icon: integrationList["gcp"]?.icon,
     label: "Google Cloud Platform (GCP)",
   },
+
   {
     value: "do",
     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,
   AWSRegistryConfig,
   DORegistryConfig,
+  GARRegistryConfig,
   GCPProvisionerConfig,
   GCPRegistryConfig,
   SkipProvisionConfig,
@@ -12,6 +13,7 @@ import type {
 export type ConnectedRegistryConfig =
   | AWSRegistryConfig
   | GCPRegistryConfig
+  | GARRegistryConfig
   | DORegistryConfig
   | SkipRegistryConnection;
 
@@ -34,7 +36,7 @@ export type OnboardingState = {
   user_email: string;
   project: ProjectData | null;
   connected_source: ConnectedSourceData | null;
-  connected_registry: any | null;
+  connected_registry: ConnectedRegistryConfig | null;
   provision_resources: Partial<ProvisionerConfig> | null;
   actions: {
     restoreState: (state: OnboardingState) => void;

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

@@ -23,6 +23,7 @@ import {
 
 import {
   CredentialsForm as GCPCredentialsForm,
+  GARegistryConfig,
   SettingsForm as GCPSettingsForm,
   TestRegistryConnection as GCPTestRegistryConnection,
 } from "./_GCPRegistryForm";
@@ -42,6 +43,11 @@ const Forms = {
     settings: GCPSettingsForm,
     test_connection: GCPTestRegistryConnection,
   },
+  gar: {
+    credentials: GCPCredentialsForm,
+    settings: GARegistryConfig,
+    test_connection: GCPTestRegistryConnection,
+  },
   do: {
     credentials: DOCredentialsForm,
     settings: DOSettingsForm,
@@ -62,6 +68,12 @@ const FormTitle = {
     doc:
       "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: {
     label: "DigitalOcean Container Registry (DOCR)",
     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 InputRow from "components/form-components/InputRow";
+import SelectRow from "components/form-components/SelectRow";
 import UploadArea from "components/form-components/UploadArea";
 import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 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 { StateHandler } from "main/home/onboarding/state/StateHandler";
 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<{
   nextFormStep: () => void;
   project: any;

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

@@ -203,7 +203,7 @@ const ProvisionResources: React.FC<{}> = () => {
       case "aws":
         return ["eks", "ecr"];
       case "gcp":
-        return ["gke", "gcr"];
+        return ["gke", "gcr", "gar"];
       case "do":
         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 Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
+import { GCP_REGION_OPTIONS } from "main/home/onboarding/constants";
 import { OFState } from "main/home/onboarding/state";
 import {
   GCPProvisionerConfig,
@@ -16,33 +17,6 @@ import { Infrastructure } from "shared/types";
 import styled from "styled-components";
 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<{
   nextFormStep: (data: Partial<GCPRegistryConfig>) => void;
   project: any;
@@ -275,7 +249,7 @@ export const SettingsForm: React.FC<{
           .filter((infra) => infra.kind == "gke")
           .sort(sortFunc);
         const matchedGCRInfras = data
-          .filter((infra) => infra.kind == "gcr")
+          .filter((infra) => infra.kind == "gcr" || infra.kind == "gar")
           .sort(sortFunc);
 
         if (matchedGKEInfras.length > 0) {
@@ -327,7 +301,8 @@ export const SettingsForm: React.FC<{
     infras: { kind: string; status: string }[]
   ) => {
     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;
     if (snap.StateHandler.connected_registry.skip) {
       if (!hasRegistryProvisioned(infras)) {
-        registryProvisionResponse = await provisionGCR(integrationId);
+        registryProvisionResponse = await provisionGAR(integrationId);
       }
     }
     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");
 
     // 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>",
           {
             gcp_integration_id: id,
-            values: {},
+            values: {
+              gcp_region: region,
+            },
           },
           { project_id: project.id, infra_id: currGCRInfra.id }
         );
@@ -414,9 +391,11 @@ export const SettingsForm: React.FC<{
         const res = await api.provisionInfra(
           "<token>",
           {
-            kind: "gcr",
+            kind: "gar",
             gcp_integration_id: id,
-            values: {},
+            values: {
+              gcp_region: region,
+            },
           },
           { project_id: project.id }
         );
@@ -489,7 +468,7 @@ export const SettingsForm: React.FC<{
         isRequired={true}
       />
       <SelectRow
-        options={regionOptions}
+        options={GCP_REGION_OPTIONS}
         width="100%"
         value={region}
         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 = {
   skip: false;
   provider: "do";

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

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

@@ -410,6 +410,7 @@ export type InfraKind =
   | "s3"
   | "gke"
   | "gcr"
+  | "gar"
   | "doks"
   | "docr"
   | "aks"
@@ -528,6 +529,13 @@ export const KindMap: ProviderInfoMap = {
     resource_link: "/integrations/registry",
     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: {
     provider: "gcp",
     source: "porter/gcp/gke",

+ 7 - 3
go.mod

@@ -3,7 +3,7 @@ module github.com/porter-dev/porter
 go 1.18
 
 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/Masterminds/semver/v3 v3.1.1
 	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/net v0.0.0-20220624214902-1bab6f366d9e
 	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/grpc v1.47.0
 	google.golang.org/protobuf v1.28.0
@@ -74,6 +74,9 @@ 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/sdk/azcore v0.23.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/golang-jwt/jwt v3.2.1+incompatible // 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-retryablehttp v0.7.1 // 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/shlex v0.0.0-20191202100458-e7afc7fbc510 // 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/gorilla/mux v1.8.0 // 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.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.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.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.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.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.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.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/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.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 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.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.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
 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=
 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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 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.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.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
 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.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.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=
 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/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 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-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-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/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/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/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 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/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-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/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/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
 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-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-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-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-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-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-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-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/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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-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-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/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=
@@ -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.62.0 h1:PhGymJMXfGBzc4lBRmrx9+1w4w2wEzURHNGF/sD/xGc=
 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.4.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-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-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-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 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-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-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-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/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/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 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.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.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.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
 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/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 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 strings.Contains(domain, "docker.io") {
 		regName = "index.docker.io/" + path
+	} else if strings.Contains(domain, "pkg.dev") {
+		regName = domain + "/" + strings.Split(path, "/")[0]
 	} else {
 		regName = domain
 

+ 5 - 1
internal/models/registry.go

@@ -45,7 +45,11 @@ func (r *Registry) ToRegistryType() *types.Registry {
 	if r.AWSIntegrationID != 0 {
 		serv = types.ECR
 	} 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 {
 		serv = types.DOCR
 	} else if r.AzureIntegrationID != 0 {

+ 232 - 1
internal/registry/registry.go

@@ -11,6 +11,7 @@ import (
 	"sync"
 	"time"
 
+	artifactregistry "cloud.google.com/go/artifactregistry/apiv1beta2"
 	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"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/repository"
 	"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"
 
@@ -70,7 +75,11 @@ func (r *Registry) ListRepositories(
 	}
 
 	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 {
@@ -203,6 +212,103 @@ func (r *Registry) listGCRRepositories(
 	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) {
 	aws, err := repo.AWSIntegration().ReadAWSIntegration(
 		r.ProjectID,
@@ -589,6 +695,8 @@ func (r *Registry) CreateRepository(
 	// if aws, create repository
 	if r.AWSIntegrationID != 0 {
 		return r.createECRRepository(repo, name)
+	} else if r.GCPIntegrationID != 0 && strings.Contains(r.URL, "pkg.dev") {
+		return r.createGARRepository(repo, name)
 	}
 
 	// otherwise, no-op
@@ -635,6 +743,62 @@ func (r *Registry) createECRRepository(
 	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
 func (r *Registry) ListImages(
 	repoName string,
@@ -651,6 +815,10 @@ func (r *Registry) ListImages(
 	}
 
 	if r.GCPIntegrationID != 0 {
+		if strings.Contains(r.URL, "pkg.dev") {
+			return r.listGARImages(repoName, repo)
+		}
+
 		return r.listGCRImages(repoName, repo)
 	}
 
@@ -1003,6 +1171,69 @@ func (r *Registry) listGCRImages(repoName string, repo repository.Repository) ([
 	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(
 	repoName string,
 	repo repository.Repository,

+ 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,
 			},
 		))
-	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(
 			&analytics.RegistryProvisioningStartTrackOpts{
 				ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(0, infra.ProjectID),

+ 14 - 0
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)
 	case string(types.InfraGCR):
 		_, err = createGCRRegistry(c.Config, infra, operation, req.Output)
+	case string(types.InfraGAR):
+		_, err = createGARRegistry(c.Config, infra, operation, req.Output)
 	case string(types.InfraACR):
 		_, err = createACRRegistry(c.Config, infra, operation, req.Output)
 	}
@@ -367,6 +369,18 @@ func createGCRRegistry(config *config.Config, infra *models.Infra, operation *mo
 	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) {
 	reg := &models.Registry{
 		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 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)
 	case types.InfraEKS, types.InfraDOKS, types.InfraGKE, types.InfraAKS:
 		_, err = deleteCluster(c.Config, infra, operation)