Parcourir la source

Merge branch 'master' into nafees/preview-env-improvements

Mohammed Nafees il y a 4 ans
Parent
commit
715610c034
55 fichiers modifiés avec 15701 ajouts et 124 suppressions
  1. 1 1
      .github/workflows/dev.yaml
  2. 8 4
      .github/workflows/prerelease.yaml
  3. 2 1
      .github/workflows/production.yaml
  4. 1 1
      .github/workflows/staging.yaml
  5. 19 0
      api/client/registry.go
  6. 102 0
      api/server/handlers/infra/forms.go
  7. 66 0
      api/server/handlers/project_integration/create_azure.go
  8. 37 0
      api/server/handlers/registry/create.go
  9. 54 0
      api/server/handlers/registry/get_token.go
  10. 1 5
      api/server/handlers/webhook/github_incoming.go
  11. 28 0
      api/server/router/project.go
  12. 28 0
      api/server/router/project_integration.go
  13. 32 0
      api/types/project_integration.go
  14. 9 0
      api/types/registry.go
  15. 1 1
      build/Dockerfile.osx
  16. 1 1
      build/Dockerfile.win
  17. 1 0
      cli/cmd/apply.go
  18. 30 0
      cli/cmd/docker/auth.go
  19. 44 0
      cli/cmd/preview/os_env_driver.go
  20. 14098 1
      dashboard/package-lock.json
  21. 1 0
      dashboard/package.json
  22. 279 0
      dashboard/src/assets/devicons-name-list.ts
  23. 10 9
      dashboard/src/components/repo-selector/BuildpackSelection.tsx
  24. 6 0
      dashboard/src/index.tsx
  25. 1 6
      dashboard/src/main/Main.tsx
  26. 9 10
      dashboard/src/main/home/cluster-dashboard/expanded-chart/BuildSettingsTab.tsx
  27. 24 13
      dashboard/src/main/home/onboarding/Routes.tsx
  28. 32 59
      dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx
  29. 37 0
      dashboard/src/shared/error_handling/logger.ts
  30. 5 1
      dashboard/src/shared/error_handling/sentry/setup.ts
  31. 13 1
      dashboard/webpack.config.js
  32. 1 1
      docker/Dockerfile
  33. 1 1
      docker/cli.Dockerfile
  34. 1 1
      docker/dev.Dockerfile
  35. 1 1
      ee/docker/ee.Dockerfile
  36. 1 1
      ee/docker/provisioner.Dockerfile
  37. 10 0
      ee/integrations/vault/types.go
  38. 38 0
      ee/integrations/vault/vault.go
  39. 13 1
      go.mod
  40. 25 0
      go.sum
  41. 0 2
      internal/kubernetes/prometheus/metrics.go
  42. 56 0
      internal/models/integrations/azure.go
  43. 1 0
      internal/models/project.go
  44. 4 0
      internal/models/registry.go
  45. 262 0
      internal/registry/registry.go
  46. 12 0
      internal/repository/credentials/credentials.go
  47. 230 0
      internal/repository/gorm/auth.go
  48. 1 0
      internal/repository/gorm/migrate.go
  49. 6 0
      internal/repository/gorm/repository.go
  50. 9 0
      internal/repository/integrations.go
  51. 1 0
      internal/repository/repository.go
  52. 40 0
      internal/repository/test/auth.go
  53. 6 0
      internal/repository/test/repository.go
  54. 1 1
      services/cli_install_script_container/Dockerfile
  55. 1 1
      services/porter_cli_container/dev.Dockerfile

+ 1 - 1
.github/workflows/dev.yaml

@@ -39,7 +39,7 @@ jobs:
           ADDON_CHART_REPO_URL=https://chart-addons.dev.getporter.dev
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
-          SENTRY_ENV=development
+          SENTRY_ENV=frontend-development
           EOL
       - name: Build
         run: |

+ 8 - 4
.github/workflows/prerelease.yaml

@@ -52,7 +52,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v2
         with:
-          go-version: 1.17
+          go-version: 1.18
       - name: Write Dashboard Environment Variables
         run: |
           cat >./dashboard/.env <<EOL
@@ -119,7 +119,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v2
         with:
-          go-version: 1.17
+          go-version: 1.18
       - name: Write Dashboard Environment Variables
         run: |
           cat >./dashboard/.env <<EOL
@@ -251,8 +251,8 @@ jobs:
     name: Zip binaries, create release and upload assets
     runs-on: ubuntu-latest
     needs:
-    - notarize
-    - build-linux
+      - notarize
+      - build-linux
     steps:
       - name: Get tag name
         id: tag_name
@@ -517,3 +517,7 @@ jobs:
         run: gh workflow run porter_test_docker_production.yml --repo porter-dev/new-release-tests
         env:
           GITHUB_TOKEN: ${{ secrets.PORTER_DEV_GITHUB_TOKEN }}
+      - name: Run test_porter_cli.yml workflow
+        run: gh workflow run test_porter_cli.yml --repo porter-dev/new-release-tests
+        env:
+          GITHUB_TOKEN: ${{ secrets.PORTER_DEV_GITHUB_TOKEN }}

+ 2 - 1
.github/workflows/production.yaml

@@ -37,6 +37,7 @@ jobs:
           DISCORD_CID=${{secrets.DISCORD_CID}}
           FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
           IS_HOSTED=true
+          ENABLE_COHERE=true
           COHERE_KEY=${{secrets.COHERE_KEY}}
           INTERCOM_APP_ID=${{secrets.INTERCOM_APP_ID}}
           INTERCOM_SRC=${{secrets.INTERCOM_SRC}}
@@ -46,7 +47,7 @@ jobs:
           ADDON_CHART_REPO_URL=https://chart-addons.getporter.dev
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
-          SENTRY_ENV=production
+          SENTRY_ENV=frontend-production
           EOL
       - name: Build
         run: |

+ 1 - 1
.github/workflows/staging.yaml

@@ -45,7 +45,7 @@ jobs:
           ADDON_CHART_REPO_URL=https://chart-addons.staging.getporter.dev
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
-          SENTRY_ENV=staging
+          SENTRY_ENV=frontend-staging
           EOL
       - name: Build
         run: |

+ 19 - 0
api/client/registry.go

@@ -123,6 +123,25 @@ func (c *Client) GetGCRAuthorizationToken(
 	return resp, err
 }
 
+// GetACRAuthorizationToken gets a ACR authorization token
+func (c *Client) GetACRAuthorizationToken(
+	ctx context.Context,
+	projectID uint,
+) (*types.GetRegistryTokenResponse, error) {
+	resp := &types.GetRegistryTokenResponse{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/registries/acr/token",
+			projectID,
+		),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}
+
 // GetDockerhubAuthorizationToken gets a Docker Hub authorization token
 func (c *Client) GetDockerhubAuthorizationToken(
 	ctx context.Context,

+ 102 - 0
api/server/handlers/infra/forms.go

@@ -361,12 +361,16 @@ tabs:
         options:
         - label: t2.medium
           value: t2.medium
+        - label: t2.large
+          value: t2.large
         - label: t2.xlarge
           value: t2.xlarge
         - label: t2.2xlarge
           value: t2.2xlarge
         - label: t3.medium
           value: t3.medium
+        - label: t3.large
+          value: t3.large
         - label: t3.xlarge
           value: t3.xlarge
         - label: t3.2xlarge
@@ -381,15 +385,113 @@ tabs:
       required: true
       placeholder: my-cluster
       variable: cluster_name
+    - type: number-input
+      label: Minimum number of EC2 instances to create in the application autoscaling group.
+      variable: min_instances
+      placeholder: "ex: 1"
+      settings:
+        default: 1
     - type: number-input
       label: Maximum number of EC2 instances to create in the application autoscaling group.
       variable: max_instances
       placeholder: "ex: 10"
       settings:
         default: 10
+- name: additional_nodegroup
+  label: Additional Node Groups
+  sections:
+  - name: is_additional_enabled
+    contents:
+    - type: heading
+      label: Additional Node Groups
+    - type: checkbox
+      variable: additional_nodegroup_enabled
+      label: Enable an additional node group for this cluster.
+      settings:
+        default: false
+  - name: additional_settings
+    show_if: additional_nodegroup_enabled
+    contents:
+    - type: string-input
+      label: Label for this node group.
+      variable: additional_nodegroup_label
+      placeholder: "ex: porter.run/workload-kind=job"
+      settings:
+        default: porter.run/workload-kind=database
+    - type: string-input
+      label: Taint for this node group.
+      variable: additional_nodegroup_taint
+      placeholder: "ex: porter.run/workload-kind=job:NoSchedule"
+      settings:
+        default: porter.run/workload-kind=database:NoSchedule
+    - type: checkbox
+      variable: additional_stateful_nodegroup_enabled
+      label: Stateful Workload
+      settings:
+        default: false
+    - type: select
+      label: ⚙️ AWS System Machine Type
+      variable: additional_nodegroup_machine_type
+      settings:
+        default: t2.medium
+        options:
+        - label: t2.medium
+          value: t2.medium
+        - label: t2.large
+          value: t2.large
+        - label: t2.xlarge
+          value: t2.xlarge
+        - label: t2.2xlarge
+          value: t2.2xlarge
+        - label: t3.medium
+          value: t3.medium
+        - label: t3.large
+          value: t3.large
+        - label: t3.xlarge
+          value: t3.xlarge
+        - label: t3.2xlarge
+          value: t3.2xlarge
+    - type: number-input
+      label: Minimum number of EC2 instances to create in the application autoscaling group.
+      variable: additional_nodegroup_min_instances
+      placeholder: "ex: 1"
+      settings:
+        default: 1
+    - type: number-input
+      label: Maximum number of EC2 instances to create in the application autoscaling group.
+      variable: additional_nodegroup_max_instances
+      placeholder: "ex: 10"
+      settings:
+        default: 10
 - name: advanced
   label: Advanced
   sections:
+  - name: system_machine_type
+    contents:
+    - type: heading
+      label: System Machine Type Settings
+    - type: select
+      label: ⚙️ AWS System Machine Type
+      variable: system_machine_type
+      settings:
+        default: t2.medium
+        options:
+        - label: t2.medium
+          value: t2.medium
+        - label: t2.large
+          value: t2.large
+        - label: t2.xlarge
+          value: t2.xlarge
+        - label: t2.2xlarge
+          value: t2.2xlarge
+        - label: t3.medium
+          value: t3.medium
+        - label: t3.large
+          value: t3.large
+        - label: t3.xlarge
+          value: t3.xlarge
+        - label: t3.2xlarge
+          value: t3.2xlarge
   - name: spot_instance_should_enable
     contents:
     - type: heading

+ 66 - 0
api/server/handlers/project_integration/create_azure.go

@@ -0,0 +1,66 @@
+package project_integration
+
+import (
+	"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"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+type CreateAzureHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCreateAzureHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateAzureHandler {
+	return &CreateAzureHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *CreateAzureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.CreateAzureRequest{}
+
+	if ok := p.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	az := CreateAzureIntegration(request, project.ID, user.ID)
+
+	az, err := p.Repo().AzureIntegration().CreateAzureIntegration(az)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := types.CreateAzureResponse{
+		AzureIntegration: az.ToAzureIntegrationType(),
+	}
+
+	p.WriteResult(w, r, res)
+}
+
+func CreateAzureIntegration(request *types.CreateAzureRequest, projectID, userID uint) *ints.AzureIntegration {
+	resp := &ints.AzureIntegration{
+		UserID:                 userID,
+		ProjectID:              projectID,
+		AzureClientID:          request.AzureClientID,
+		AzureSubscriptionID:    request.AzureSubscriptionID,
+		AzureTenantID:          request.AzureTenantID,
+		ServicePrincipalSecret: []byte(request.ServicePrincipalKey),
+	}
+
+	return resp
+}

+ 37 - 0
api/server/handlers/registry/create.go

@@ -1,6 +1,7 @@
 package registry
 
 import (
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -57,6 +58,7 @@ func (p *RegistryCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		AWSIntegrationID:   request.AWSIntegrationID,
 		DOIntegrationID:    request.DOIntegrationID,
 		BasicIntegrationID: request.BasicIntegrationID,
+		AzureIntegrationID: request.AzureIntegrationID,
 	}
 
 	if regModel.URL == "" && regModel.AWSIntegrationID != 0 {
@@ -68,6 +70,41 @@ func (p *RegistryCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		}
 
 		regModel.URL = url
+	} else if request.AzureIntegrationID != 0 {
+		// if azure integration id is non-zero check that resource group name and repo name are set
+		if request.ACRName == "" {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("acr_name must be set if azure_integration_id is not 0"),
+				http.StatusBadRequest,
+			))
+
+			return
+		} else if request.ACRResourceGroupName == "" {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("acr_resource_group_name must be set if azure_integration_id is not 0"),
+				http.StatusBadRequest,
+			))
+
+			return
+		}
+
+		// get the azure integration and overwrite the names
+		az, err := p.Repo().AzureIntegration().ReadAzureIntegration(proj.ID, request.AzureIntegrationID)
+
+		if err != nil {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		az.ACRName = request.ACRName
+		az.ACRResourceGroupName = request.ACRResourceGroupName
+
+		az, err = p.Repo().AzureIntegration().OverwriteAzureIntegration(az)
+
+		if err != nil {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 	}
 
 	// handle write to the database

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

@@ -293,3 +293,57 @@ func (c *RegistryGetDockerhubTokenHandler) ServeHTTP(w http.ResponseWriter, r *h
 
 	c.WriteResult(w, r, resp)
 }
+
+type RegistryGetACRTokenHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewRegistryGetACRTokenHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *RegistryGetACRTokenHandler {
+	return &RegistryGetACRTokenHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *RegistryGetACRTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	// 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.AzureIntegrationID != 0 && strings.Contains(reg.URL, "azurecr.io") {
+			_reg := registry.Registry(*reg)
+
+			username, pw, err := _reg.GetACRCredentials(c.Repo())
+
+			if err != nil {
+				continue
+			}
+
+			token = base64.StdEncoding.EncodeToString([]byte(string(username) + ":" + string(pw)))
+
+			// we'll just set an arbitrary 30-day expiry time (this is not enforced)
+			timeExpires := time.Now().Add(30 * 24 * 3600 * time.Second)
+			expiresAt = &timeExpires
+		}
+	}
+
+	resp := &types.GetRegistryTokenResponse{
+		Token:     token,
+		ExpiresAt: expiresAt,
+	}
+
+	c.WriteResult(w, r, resp)
+}

+ 1 - 5
api/server/handlers/webhook/github_incoming.go

@@ -174,7 +174,7 @@ func (c *GithubIncomingWebhookHandler) deleteDeployment(
 		State: &state,
 	}
 
-	_, _, err = client.Repositories.CreateDeploymentStatus(
+	client.Repositories.CreateDeploymentStatus(
 		context.Background(),
 		env.GitRepoOwner,
 		env.GitRepoName,
@@ -182,10 +182,6 @@ func (c *GithubIncomingWebhookHandler) deleteDeployment(
 		&deploymentStatusRequest,
 	)
 
-	if err != nil {
-		return err
-	}
-
 	depl.Status = types.DeploymentStatusInactive
 
 	// update the deployment to mark it inactive

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

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

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

@@ -272,5 +272,33 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/integrations/azure -> project_integration.NewCreateAzureHandler
+	createAzureEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/azure",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	createAzureHandler := project_integration.NewCreateAzureHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: createAzureEndpoint,
+		Handler:  createAzureHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 32 - 0
api/types/project_integration.go

@@ -125,3 +125,35 @@ type CreateGCPRequest struct {
 type CreateGCPResponse struct {
 	*GCPIntegration
 }
+
+type AzureIntegration struct {
+	CreatedAt time.Time `json:"created_at"`
+
+	ID uint `json:"id"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The Azure client ID that this is linked to
+	AzureClientID string `json:"azure_client_id"`
+
+	// The Azure subscription ID that this is linked to
+	AzureSubscriptionID string `json:"azure_subscription_id"`
+
+	// The Azure tenant ID that this is linked to
+	AzureTenantID string `json:"azure_tenant_id"`
+}
+
+type CreateAzureRequest struct {
+	AzureClientID       string `json:"azure_client_id" form:"required"`
+	AzureSubscriptionID string `json:"azure_subscription_id" form:"required"`
+	AzureTenantID       string `json:"azure_tenant_id" form:"required"`
+	ServicePrincipalKey string `json:"service_principal_key" form:"required"`
+}
+
+type CreateAzureResponse struct {
+	*AzureIntegration
+}

+ 9 - 0
api/types/registry.go

@@ -27,6 +27,9 @@ type Registry struct {
 	// The AWS integration that was used to create or connect the registry
 	AWSIntegrationID uint `json:"aws_integration_id,omitempty"`
 
+	// The Azure integration that was used to create or connect the registry
+	AzureIntegrationID uint `json:"azure_integration_id,omitempty"`
+
 	// The GCP integration that was used to create or connect the registry
 	GCPIntegrationID uint `json:"gcp_integration_id,omitempty"`
 
@@ -73,6 +76,7 @@ type RegistryService string
 const (
 	GCR       RegistryService = "gcr"
 	ECR       RegistryService = "ecr"
+	ACR       RegistryService = "acr"
 	DOCR      RegistryService = "docr"
 	DockerHub RegistryService = "dockerhub"
 )
@@ -86,6 +90,11 @@ type CreateRegistryRequest struct {
 	AWSIntegrationID   uint   `json:"aws_integration_id"`
 	DOIntegrationID    uint   `json:"do_integration_id"`
 	BasicIntegrationID uint   `json:"basic_integration_id"`
+	AzureIntegrationID uint   `json:"azure_integration_id"`
+
+	// Additional Azure-specific fields
+	ACRResourceGroupName string `json:"acr_resource_group_name"`
+	ACRName              string `json:"acr_name"`
 }
 
 type CreateRegistryRepositoryRequest struct {

+ 1 - 1
build/Dockerfile.osx

@@ -1,4 +1,4 @@
-ARG GO_VERSION=1.17
+ARG GO_VERSION=1.18
 
 FROM golang:${GO_VERSION}
 

+ 1 - 1
build/Dockerfile.win

@@ -1,4 +1,4 @@
-ARG GO_VERSION=1.17
+ARG GO_VERSION=1.18
 
 FROM golang:${GO_VERSION}
 

+ 1 - 0
cli/cmd/apply.go

@@ -104,6 +104,7 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []str
 	worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
 	worker.RegisterDriver("random-string", preview.NewRandomStringDriver)
 	worker.RegisterDriver("env-group", preview.NewEnvGroupDriver)
+	worker.RegisterDriver("os-env", preview.NewOSEnvDriver)
 
 	worker.SetDefaultDriver("deploy")
 

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

@@ -55,6 +55,8 @@ func (a *AuthGetter) GetCredentials(serverURL string) (user string, secret strin
 		return a.GetDOCRCredentials(serverURL, a.ProjectID)
 	} else if strings.Contains(serverURL, "index.docker.io") {
 		return a.GetDockerHubCredentials(serverURL, a.ProjectID)
+	} else if strings.Contains(serverURL, "azurecr.io") {
+		return a.GetACRCredentials(serverURL, a.ProjectID)
 	}
 
 	return a.GetECRCredentials(serverURL, a.ProjectID)
@@ -204,6 +206,34 @@ func (a *AuthGetter) GetDockerHubCredentials(serverURL string, projID uint) (use
 	return decodeDockerToken(token)
 }
 
+func (a *AuthGetter) GetACRCredentials(serverURL string, projID uint) (user string, secret string, err error) {
+	cachedEntry := a.Cache.Get(serverURL)
+	var token string
+
+	if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
+		token = cachedEntry.AuthorizationToken
+	} else {
+		// get a token from the server
+		tokenResp, err := a.Client.GetACRAuthorizationToken(context.Background(), projID)
+
+		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 decodeDockerToken(token)
+}
+
 func decodeDockerToken(token string) (string, string, error) {
 	decodedToken, err := base64.StdEncoding.DecodeString(token)
 

+ 44 - 0
cli/cmd/preview/os_env_driver.go

@@ -0,0 +1,44 @@
+package preview
+
+import (
+	"os"
+	"strings"
+
+	"github.com/porter-dev/switchboard/pkg/drivers"
+	"github.com/porter-dev/switchboard/pkg/models"
+)
+
+type OSEnvDriver struct {
+	output map[string]interface{}
+}
+
+func NewOSEnvDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
+	return &OSEnvDriver{
+		output: make(map[string]interface{}),
+	}, nil
+}
+
+func (d *OSEnvDriver) ShouldApply(resource *models.Resource) bool {
+	return true
+}
+
+func (d *OSEnvDriver) Apply(resource *models.Resource) (*models.Resource, error) {
+	for _, key := range os.Environ() {
+		keyVal := strings.Split(key, "=")
+
+		if len(keyVal) == 2 && keyVal[0] != "" && keyVal[1] != "" &&
+			strings.HasPrefix(keyVal[0], "PORTER_APPLY_") {
+			envName := strings.TrimPrefix(keyVal[0], "PORTER_APPLY_")
+
+			if len(envName) > 0 {
+				d.output[envName] = keyVal[1]
+			}
+		}
+	}
+
+	return resource, nil
+}
+
+func (d *OSEnvDriver) Output() (map[string]interface{}, error) {
+	return d.output, nil
+}

Fichier diff supprimé car celui-ci est trop grand
+ 14098 - 1
dashboard/package-lock.json


+ 1 - 0
dashboard/package.json

@@ -24,6 +24,7 @@
     "brace": "^0.11.1",
     "clipboard": "^2.0.8",
     "cohere-js": "^1.0.19",
+    "cohere-sentry": "^1.0.1",
     "core-js": "^3.16.1",
     "cron-parser": "^4.3.0",
     "cron-validator": "^1.3.1",

+ 279 - 0
dashboard/src/assets/devicons-name-list.ts

@@ -0,0 +1,279 @@
+export const DeviconsNameList = [
+  { name: "adonisjs" },
+  { name: "aftereffects" },
+  { name: "amazonwebservices" },
+  { name: "android" },
+  { name: "androidstudio" },
+  { name: "aarch64" },
+  { name: "angularjs" },
+  { name: "ansible" },
+  { name: "apache" },
+  { name: "apachekafka" },
+  { name: "appcelerator" },
+  { name: "apple" },
+  { name: "appwrite" },
+  { name: "arduino" },
+  { name: "atom" },
+  { name: "azure" },
+  { name: "babel" },
+  { name: "backbonejs" },
+  { name: "bamboo" },
+  { name: "bash" },
+  { name: "behance" },
+  { name: "bitbucket" },
+  { name: "bootstrap" },
+  { name: "bulma" },
+  { name: "bower" },
+  { name: "c" },
+  { name: "cakephp" },
+  { name: "canva" },
+  { name: "centos" },
+  { name: "ceylon" },
+  { name: "chrome" },
+  { name: "circleci" },
+  { name: "clojure" },
+  { name: "cmake" },
+  { name: "clojurescript" },
+  { name: "codecov" },
+  { name: "codeigniter" },
+  { name: "codepen" },
+  { name: "coffeescript" },
+  { name: "composer" },
+  { name: "confluence" },
+  { name: "couchdb" },
+  { name: "cplusplus" },
+  { name: "csharp" },
+  { name: "css3" },
+  { name: "cucumber" },
+  { name: "crystal" },
+  { name: "d3js" },
+  { name: "dart" },
+  { name: "debian" },
+  { name: "denojs" },
+  { name: "devicon" },
+  { name: "django" },
+  { name: "docker" },
+  { name: "doctrine" },
+  { name: "dot-net" },
+  { name: "dotnetcore" },
+  { name: "drupal" },
+  { name: "digitalocean" },
+  { name: "discordjs" },
+  { name: "electron" },
+  { name: "eleventy" },
+  { name: "elixir" },
+  { name: "elm" },
+  { name: "ember" },
+  { name: "embeddedc" },
+  { name: "erlang" },
+  { name: "eslint" },
+  { name: "express" },
+  { name: "facebook" },
+  { name: "feathersjs" },
+  { name: "figma" },
+  { name: "filezilla" },
+  { name: "firebase" },
+  { name: "firefox" },
+  { name: "flask" },
+  { name: "flutter" },
+  { name: "foundation" },
+  { name: "fsharp" },
+  { name: "gatling" },
+  { name: "gatsby" },
+  { name: "rect" },
+  { name: "gcc" },
+  { name: "gentoo" },
+  { name: "gimp" },
+  { name: "git" },
+  { name: "github" },
+  { name: "gitlab" },
+  { name: "gitter" },
+  { name: "go" },
+  { name: "google" },
+  { name: "googlecloud" },
+  { name: "gradle" },
+  { name: "grafana" },
+  { name: "grails" },
+  { name: "graphql" },
+  { name: "groovy" },
+  { name: "grunt" },
+  { name: "gulp" },
+  { name: "godot" },
+  { name: "haskell" },
+  { name: "handlebars" },
+  { name: "haxe" },
+  { name: "heroku" },
+  { name: "html5" },
+  { name: "hugo" },
+  { name: "ie10" },
+  { name: "ifttt" },
+  { name: "illustrator" },
+  { name: "inkscape" },
+  { name: "intellij" },
+  { name: "ionic" },
+  { name: "jamstack" },
+  { name: "jasmine" },
+  { name: "java" },
+  { name: "javascript" },
+  { name: "jeet" },
+  { name: "jest" },
+  { name: "jenkins" },
+  { name: "jetbrains" },
+  { name: "jira" },
+  { name: "jquery" },
+  { name: "julia" },
+  { name: "jupyter" },
+  { name: "kaggle" },
+  { name: "karma" },
+  { name: "kotlin" },
+  { name: "knockout" },
+  { name: "krakenjs" },
+  { name: "kubernetes" },
+  { name: "labview" },
+  { name: "laravel" },
+  { name: "latex" },
+  { name: "less" },
+  { name: "linkedin" },
+  { name: "lua" },
+  { name: "linux" },
+  { name: "materialui" },
+  { name: "matlab" },
+  { name: "magento" },
+  { name: "markdown" },
+  { name: "maya" },
+  { name: "meteor" },
+  { name: "minitab" },
+  { name: "mocha" },
+  { name: "modx" },
+  { name: "mongodb" },
+  { name: "moodle" },
+  { name: "msdos" },
+  { name: "mysql" },
+  { name: "neo4j" },
+  { name: "nestjs" },
+  { name: "networkx" },
+  { name: "nextjs" },
+  { name: "nginx" },
+  { name: "nixos" },
+  { name: "nodejs" },
+  { name: "nodewebkit" },
+  { name: "npm" },
+  { name: "nuget" },
+  { name: "numpy" },
+  { name: "nuxtjs" },
+  { name: "objectivec" },
+  { name: "opera" },
+  { name: "ocaml" },
+  { name: "openal" },
+  { name: "opengl" },
+  { name: "opensuse" },
+  { name: "oracle" },
+  { name: "pandas" },
+  { name: "perl" },
+  { name: "phalcon" },
+  { name: "photoshop" },
+  { name: "php" },
+  { name: "phpstorm" },
+  { name: "podman" },
+  { name: "polygon" },
+  { name: "postgresql" },
+  { name: "premierepro" },
+  { name: "processing" },
+  { name: "protractor" },
+  { name: "putty" },
+  { name: "pycharm" },
+  { name: "python" },
+  { name: "pytorch" },
+  { name: "raspberrypi" },
+  { name: "phoenix" },
+  { name: "qt" },
+  { name: "r" },
+  { name: "rails" },
+  { name: "react" },
+  { name: "redhat" },
+  { name: "redis" },
+  { name: "redux" },
+  { name: "rocksdb" },
+  { name: "ruby" },
+  { name: "rubymine" },
+  { name: "rust" },
+  { name: "safari" },
+  { name: "salesforce" },
+  { name: "sdl" },
+  { name: "rstudio" },
+  { name: "sass" },
+  { name: "scala" },
+  { name: "selenium" },
+  { name: "sequelize" },
+  { name: "shopware" },
+  { name: "shotgrid" },
+  { name: "sketch" },
+  { name: "slack" },
+  { name: "socketio" },
+  { name: "solidity" },
+  { name: "sourcetree" },
+  { name: "spring" },
+  { name: "spss" },
+  { name: "sqlalchemy" },
+  { name: "sqlite" },
+  { name: "subversion" },
+  { name: "microsoftsqlserver" },
+  { name: "ssh" },
+  { name: "stylus" },
+  { name: "svelte" },
+  { name: "swift" },
+  { name: "symfony" },
+  { name: "storybook" },
+  { name: "tailwindcss" },
+  { name: "tensorflow" },
+  { name: "terraform" },
+  { name: "threejs" },
+  { name: "tomcat" },
+  { name: "tortoisegit" },
+  { name: "towergit" },
+  { name: "travis" },
+  { name: "thealgorithms" },
+  { name: "trello" },
+  { name: "twitter" },
+  { name: "typescript" },
+  { name: "typo3" },
+  { name: "ubuntu" },
+  { name: "unity" },
+  { name: "unix" },
+  { name: "unrealengine" },
+  { name: "uwsgi" },
+  { name: "vagrant" },
+  { name: "vim" },
+  { name: "visualstudio" },
+  { name: "vuejs" },
+  { name: "vuestorefront" },
+  { name: "vscode" },
+  { name: "webflow" },
+  { name: "weblate" },
+  { name: "webpack" },
+  { name: "webstorm" },
+  { name: "windows8" },
+  { name: "woocommerce" },
+  { name: "wordpress" },
+  { name: "xamarin" },
+  { name: "xcode" },
+  { name: "xd" },
+  { name: "yarn" },
+  { name: "yii" },
+  { name: "yunohost" },
+  { name: "zend" },
+  { name: "zig" },
+  { name: "pytest" },
+  { name: "opencv" },
+  { name: "fastapi" },
+  { name: "k3s" },
+  { name: "packer" },
+  { name: "anaconda" },
+  { name: "rspec" },
+  { name: "argocd" },
+  { name: "prometheus" },
+  { name: "blender" },
+  { name: "dropwizard" },
+  { name: "vuetify" },
+  { name: "fedora" },
+];

+ 10 - 9
dashboard/src/components/repo-selector/BuildpackSelection.tsx

@@ -1,3 +1,4 @@
+import { DeviconsNameList } from "assets/devicons-name-list";
 import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import SelectRow from "components/form-components/SelectRow";
@@ -12,8 +13,6 @@ const DEFAULT_BUILDER_NAME = "heroku";
 const DEFAULT_PAKETO_STACK = "paketobuildpacks/builder:full";
 const DEFAULT_HEROKU_STACK = "heroku/buildpacks:20";
 
-const URLRegex = /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/;
-
 type BuildConfig = {
   builder: string;
   buildpacks: string[];
@@ -179,9 +178,16 @@ export const BuildpackSelection: React.FC<{
     action: "remove" | "add"
   ) => {
     return buildpacks?.map((buildpack) => {
-      const icon = `devicon-${buildpack?.name?.toLowerCase()}-plain colored`;
+      const [languageName] = buildpack.name?.split("/").reverse();
+
+      const devicon = DeviconsNameList.find(
+        (devicon) => languageName.toLowerCase() === devicon.name
+      );
+
+      const icon = `devicon-${devicon?.name}-plain colored`;
+
       let disableIcon = false;
-      if (URLRegex.test(buildpack.buildpack)) {
+      if (!devicon) {
         disableIcon = true;
       }
 
@@ -314,11 +320,6 @@ export const AddCustomBuildpackForm: React.FC<{
   const [error, setError] = useState(false);
 
   const handleAddCustomBuildpack = () => {
-    if (!URLRegex.test(buildpackUrl)) {
-      setError(true);
-      return;
-    }
-
     const buildpack: Buildpack = {
       buildpack: buildpackUrl,
       name: buildpackUrl,

+ 6 - 0
dashboard/src/index.tsx

@@ -3,6 +3,7 @@ import "regenerator-runtime/runtime";
 
 import * as React from "react";
 import * as ReactDOM from "react-dom";
+import Cohere from "cohere-js";
 import App from "./App";
 import { SetupSentry } from "shared/error_handling/sentry/setup";
 import { EnableErrorHandling } from "shared/error_handling/window_error_handling";
@@ -12,6 +13,11 @@ declare global {
     analytics: any;
   }
 }
+
+if (process.env.ENABLE_COHERE) {
+  Cohere.init(process.env.COHERE_API_KEY);
+}
+
 if (process.env.ENABLE_SENTRY) {
   SetupSentry();
 }

+ 1 - 6
dashboard/src/main/Main.tsx

@@ -4,11 +4,6 @@ import { Route, Redirect, Switch } from "react-router-dom";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import Cohere from "cohere-js";
-
-if (window.location.href.includes("dashboard.getporter.dev")) {
-  Cohere.init(process.env.COHERE_API_KEY);
-}
-
 import ResetPasswordInit from "./auth/ResetPasswordInit";
 import ResetPasswordFinalize from "./auth/ResetPasswordFinalize";
 import Login from "./auth/Login";
@@ -47,7 +42,7 @@ export default class Main extends Component<PropsType, StateType> {
       .checkAuth("", {}, {})
       .then((res) => {
         if (res && res?.data) {
-          if (window.location.href.includes("dashboard.getporter.dev")) {
+          if (process.env.ENABLE_COHERE) {
             Cohere.identify(res?.data?.id, {
               displayName: res?.data?.email,
               email: res?.data?.email,

+ 9 - 10
dashboard/src/main/home/cluster-dashboard/expanded-chart/BuildSettingsTab.tsx

@@ -15,13 +15,9 @@ import {
 } from "shared/types";
 import styled, { keyframes } from "styled-components";
 import yaml from "js-yaml";
-import DynamicLink from "components/DynamicLink";
 import { AxiosError } from "axios";
 import { AddCustomBuildpackForm } from "components/repo-selector/BuildpackSelection";
-
-const DEFAULT_PAKETO_STACK = "paketobuildpacks/builder:full";
-const DEFAULT_HEROKU_STACK = "heroku/buildpacks:20";
-const URLRegex = /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/;
+import { DeviconsNameList } from "assets/devicons-name-list";
 
 type Buildpack = {
   name: string;
@@ -565,13 +561,16 @@ const BuildpackConfigSection: React.FC<{
     }
 
     return buildpacks?.map((buildpack, i) => {
-      const icon = `devicon-${buildpack?.name?.toLowerCase()}-plain colored`;
+      const [languageName] = buildpack.name?.split("/").reverse();
+
+      const devicon = DeviconsNameList.find(
+        (devicon) => languageName.toLowerCase() === devicon.name
+      );
+
+      const icon = `devicon-${devicon?.name}-plain colored`;
 
       let disableIcon = false;
-      if (
-        URLRegex.test(buildpack.buildpack) &&
-        !buildpack.buildpack.includes("gcr.io/paketo-buildpacks")
-      ) {
+      if (!devicon) {
         disableIcon = true;
       }
 

+ 24 - 13
dashboard/src/main/home/onboarding/Routes.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 import { Route, Switch } from "react-router";
+import PorterErrorBoundary from "shared/error_handling/PorterErrorBoundary";
 import { OFState } from "./state";
 import ConnectRegistry from "./steps/ConnectRegistry/ConnectRegistry";
 import ConnectSource from "./steps/ConnectSource";
@@ -8,19 +9,29 @@ import ProvisionResources from "./steps/ProvisionResources/ProvisionResources";
 export const Routes = () => {
   return (
     <>
-      <Switch>
-        <Route path={`/onboarding/source`}>
-          <ConnectSource
-            onSuccess={(data) => OFState.actions.nextStep("continue", data)}
-          />
-        </Route>
-        <Route path={["/onboarding/registry/:step?"]}>
-          <ConnectRegistry />
-        </Route>
-        <Route path={[`/onboarding/provision/:step?`]}>
-          <ProvisionResources />
-        </Route>
-      </Switch>
+      <PorterErrorBoundary
+        errorBoundaryLocation="onboarding"
+        tags={{ scope: "onboarding" }}
+      >
+        <Switch>
+          <Route path={`/onboarding/source`}>
+            <ConnectSource
+              onSuccess={(data) => OFState.actions.nextStep("continue", data)}
+            />
+          </Route>
+          <Route path={["/onboarding/registry/:step?"]}>
+            <ConnectRegistry />
+          </Route>
+          <Route path={[`/onboarding/provision/:step?`]}>
+            <PorterErrorBoundary
+              errorBoundaryLocation="onboarding.provision_resources"
+              tags={{ scope: "onboarding.provision_resources" }}
+            >
+              <ProvisionResources />
+            </PorterErrorBoundary>
+          </Route>
+        </Switch>
+      </PorterErrorBoundary>
     </>
   );
 };

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

@@ -1,7 +1,8 @@
 import Helper from "components/form-components/Helper";
 import SaveButton from "components/SaveButton";
 import TitleSection from "components/TitleSection";
-import React, { useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
+import Cohere from "cohere-js";
 import { useParams } from "react-router";
 import styled from "styled-components";
 import ProviderSelector, {
@@ -22,12 +23,11 @@ import api from "shared/api";
 import Placeholder from "components/Placeholder";
 import Loading from "components/Loading";
 import MultiSaveButton from "components/MultiSaveButton";
+import buildLogger from "shared/error_handling/logger";
 
-type Props = {};
+const ProvisionResourcesLogger = buildLogger("onboarding.provision_resources");
 
-type SaveButtonOptions = "retry" | "delete_all" | "back";
-
-const ProvisionResources: React.FC<Props> = () => {
+const ProvisionResources: React.FC<{}> = () => {
   const snap = useSnapshot(OFState);
   const { step } = useParams<{ step: any }>();
   const [infraStatus, setInfraStatus] = useState<{
@@ -35,10 +35,6 @@ const ProvisionResources: React.FC<Props> = () => {
     errored_infras: number[];
     description?: string;
   }>(null);
-  const [
-    failedSaveButtonOption,
-    setFailedSaveButtonOption,
-  ] = useState<SaveButtonOptions>("retry");
 
   const [isLoading, setIsLoading] = useState(false);
 
@@ -134,49 +130,6 @@ const ProvisionResources: React.FC<Props> = () => {
       });
   };
 
-  const getFailedSaveButton = () => {
-    switch (failedSaveButtonOption) {
-      case "retry":
-        return (
-          <SaveButton
-            text="Retry"
-            disabled={false}
-            onClick={retryFailedInfras}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="right"
-            saveText=""
-          />
-        );
-      case "delete_all":
-        return (
-          <SaveButton
-            text="Delete All Infrastructure"
-            disabled={false}
-            onClick={deleteAllInfras}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="right"
-            saveText=""
-          />
-        );
-      case "back":
-        return (
-          <SaveButton
-            text="Configure Settings"
-            disabled={false}
-            onClick={() => {
-              handleGoBack("");
-            }}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="right"
-            saveText=""
-          />
-        );
-    }
-  };
-
   const renderSaveButton = () => {
     if (typeof infraStatus?.hasError !== "boolean") {
       return;
@@ -237,15 +190,15 @@ const ProvisionResources: React.FC<Props> = () => {
     }
   };
 
-  const getDescription = () => {
-    if (infraStatus && infraStatus.hasError) {
+  const description = useMemo(() => {
+    if (infraStatus?.hasError) {
       return "Error while creating infrastructure. Please select an option below to continue.";
     }
 
     return "Note: Provisioning can take up to 15 minutes.";
-  };
+  }, [infraStatus]);
 
-  const getFilterOpts = (): string[] => {
+  const filterOpts = useMemo(() => {
     switch (provider) {
       case "aws":
         return ["eks", "ecr"];
@@ -256,7 +209,27 @@ const ProvisionResources: React.FC<Props> = () => {
     }
 
     return [];
-  };
+  }, [provider]);
+
+  useEffect(() => {
+    if (!infraStatus) return;
+
+    if (typeof infraStatus.hasError !== "boolean") return;
+
+    if (infraStatus.hasError) {
+      Cohere.widget("show");
+      Cohere.widget("expand");
+      ProvisionResourcesLogger.critical(new Error(infraStatus.description));
+    } else {
+      Cohere.widget("hide");
+    }
+  }, [infraStatus]);
+
+  useEffect(() => {
+    return () => {
+      Cohere.widget("hide");
+    };
+  }, []);
 
   const Content = () => {
     switch (step) {
@@ -276,7 +249,7 @@ const ProvisionResources: React.FC<Props> = () => {
           <>
             <StatusPage
               project_id={project?.id}
-              filter={getFilterOpts()}
+              filter={filterOpts}
               setInfraStatus={setInfraStatus}
               filterLatest
               auto_expanded
@@ -285,7 +258,7 @@ const ProvisionResources: React.FC<Props> = () => {
               can_delete={false}
             />
             <Br />
-            <Helper>{getDescription()}</Helper>
+            <Helper>{description}</Helper>
             {renderSaveButton()}
           </>
         );

+ 37 - 0
dashboard/src/shared/error_handling/logger.ts

@@ -0,0 +1,37 @@
+import * as Sentry from "@sentry/react";
+
+type LogFunction = (error: Error) => void;
+type LogFunctions = {
+  [key in Sentry.Severity]: LogFunction;
+};
+
+const logFunctionBuilder = (scope: string, severity: Sentry.Severity) => (
+  error: Error
+) => {
+  Sentry.withScope((sentryScope) => {
+    sentryScope.setTag("scope", scope);
+    sentryScope.setLevel(severity);
+
+    Sentry.captureException(error);
+  });
+};
+
+function buildLogger(scope: string = "global") {
+  const logFunctions = Object.values(Sentry.Severity).reduce<LogFunctions>(
+    (acc, currentSeverity) => {
+      if (typeof currentSeverity === "string") {
+        acc[currentSeverity] = logFunctionBuilder(
+          scope,
+          Sentry.Severity.fromString(currentSeverity)
+        );
+      }
+
+      return acc;
+    },
+    {} as LogFunctions
+  );
+
+  return logFunctions;
+}
+
+export default buildLogger;

+ 5 - 1
dashboard/src/shared/error_handling/sentry/setup.ts

@@ -1,8 +1,12 @@
 import * as Sentry from "@sentry/react";
 import { Integrations } from "@sentry/tracing";
+import CohereSentry from "cohere-sentry";
 
 const SENTRY_DSN = process.env.SENTRY_DSN;
 const SENTRY_ENV = process.env.SENTRY_ENV || "development";
+const COHERE_INTEGRATION = process.env.ENABLE_COHERE
+  ? [new CohereSentry()]
+  : [];
 
 export const SetupSentry = () => {
   if (!SENTRY_DSN) {
@@ -10,7 +14,7 @@ export const SetupSentry = () => {
   }
   Sentry.init({
     dsn: SENTRY_DSN,
-    integrations: [new Integrations.BrowserTracing()],
+    integrations: [new Integrations.BrowserTracing(), ...COHERE_INTEGRATION],
     environment: SENTRY_ENV,
     // Check out https://docs.sentry.io/platforms/javascript/guides/react/configuration/sampling/ for a more refined sample rate
     tracesSampleRate: 1,

+ 13 - 1
dashboard/webpack.config.js

@@ -12,13 +12,25 @@ const TerserPlugin = require("terser-webpack-plugin");
 
 module.exports = () => {
   let env = dotenv.config().parsed;
+
   if (!env) {
     env = process.env;
   }
   const envKeys = Object.keys(env).reduce((prev, next) => {
-    prev[`process.env.${next}`] = JSON.stringify(env[next]);
+    const varName = `process.env.${next}`;
+    if (typeof env[next] !== "string") return prev;
+
+    if (env[next].toLowerCase() === "true") {
+      prev[varName] = true;
+    } else if (env[next].toLowerCase() === "false") {
+      prev[varName] = false;
+    } else {
+      prev[varName] = JSON.stringify(env[next]);
+    }
+
     return prev;
   }, {});
+
   // Check first the env file and if it's empty, check out the node env of the process.
   let isDevelopment = env.NODE_ENV !== "production";
   if (process.env.NODE_ENV !== env.NODE_ENV) {

+ 1 - 1
docker/Dockerfile

@@ -2,7 +2,7 @@
 
 # Base Go environment
 # -------------------
-FROM golang:1.17-alpine as base
+FROM golang:1.18-alpine as base
 WORKDIR /porter
 
 RUN apk update && apk add --no-cache gcc musl-dev git protoc

+ 1 - 1
docker/cli.Dockerfile

@@ -2,7 +2,7 @@
 
 # Base Go environment
 # -------------------
-FROM golang:1.17 as base
+FROM golang:1.18 as base
 WORKDIR /porter
 
 RUN apt-get update && apt-get install -y gcc musl-dev git make

+ 1 - 1
docker/dev.Dockerfile

@@ -1,6 +1,6 @@
 # Development environment
 # -----------------------
-FROM golang:1.17-alpine
+FROM golang:1.18-alpine
 WORKDIR /porter
 
 RUN apk update && apk add --no-cache gcc musl-dev git

+ 1 - 1
ee/docker/ee.Dockerfile

@@ -2,7 +2,7 @@
 
 # Base Go environment
 # -------------------
-FROM golang:1.17-alpine as base
+FROM golang:1.18-alpine as base
 WORKDIR /porter
 
 RUN apk update && apk add --no-cache gcc musl-dev git protoc

+ 1 - 1
ee/docker/provisioner.Dockerfile

@@ -2,7 +2,7 @@
 
 # Base Go environment
 # -------------------
-FROM golang:1.17-alpine as base
+FROM golang:1.18-alpine as base
 WORKDIR /porter
 
 RUN apk update && apk add --no-cache gcc musl-dev git protoc

+ 10 - 0
ee/integrations/vault/types.go

@@ -48,6 +48,16 @@ type GetAWSCredentialData struct {
 	Data     *credentials.AWSCredential `json:"data"`
 }
 
+type GetAzureCredentialResponse struct {
+	*VaultGetResponse
+	Data *GetAzureCredentialData `json:"data"`
+}
+
+type GetAzureCredentialData struct {
+	Metadata *VaultMetadata               `json:"metadata"`
+	Data     *credentials.AzureCredential `json:"data"`
+}
+
 type CreatePolicyRequest struct {
 	Policy string `json:"policy"`
 }

+ 38 - 0
ee/integrations/vault/vault.go

@@ -147,6 +147,44 @@ func (c *Client) getAWSCredentialPath(awsIntegration *integrations.AWSIntegratio
 	)
 }
 
+func (c *Client) WriteAzureCredential(
+	azIntegration *integrations.AzureIntegration,
+	data *credentials.AzureCredential) error {
+	reqData := &CreateVaultSecretRequest{
+		Data: data,
+	}
+
+	return c.postRequest(fmt.Sprintf("/v1/%s", c.getAzureCredentialPath(azIntegration)), reqData, nil)
+}
+
+func (c *Client) GetAzureCredential(azIntegration *integrations.AzureIntegration) (*credentials.AzureCredential, error) {
+	resp := &GetAzureCredentialResponse{}
+
+	err := c.getRequest(fmt.Sprintf("/v1/%s", c.getAzureCredentialPath(azIntegration)), resp)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Data.Data, nil
+}
+
+func (c *Client) CreateAzureToken(azIntegration *integrations.AzureIntegration) (string, error) {
+	credPath := c.getAzureCredentialPath(azIntegration)
+	policyName := fmt.Sprintf("access-%d-azure-%d", azIntegration.ProjectID, azIntegration.ID)
+
+	return c.getToken(credPath, policyName)
+}
+
+func (c *Client) getAzureCredentialPath(azIntegration *integrations.AzureIntegration) string {
+	return fmt.Sprintf(
+		"kv/data/secret/%s/%d/azure/%d",
+		c.secretPrefix,
+		azIntegration.ProjectID,
+		azIntegration.ID,
+	)
+}
+
 const readOnlyPolicyTemplate = `path "%s" {
   capabilities = ["read"]
 }`

+ 13 - 1
go.mod

@@ -1,6 +1,6 @@
 module github.com/porter-dev/porter
 
-go 1.17
+go 1.18
 
 require (
 	cloud.google.com/go v0.99.0
@@ -74,6 +74,18 @@ require (
 )
 
 require (
+	github.com/Azure/azure-sdk-for-go v63.4.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
+	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect
+	github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
+	github.com/kylelemons/godebug v1.1.0 // indirect
+	github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
+)
+
+require (
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.14.0
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
 	github.com/Azure/go-autorest/autorest v0.11.20 // indirect

+ 25 - 0
go.sum

@@ -55,7 +55,20 @@ github.com/AlecAivazis/survey/v2 v2.2.9 h1:LWvJtUswz/W9/zVVXELrmlvdwWcKE60ZAw0FW
 github.com/AlecAivazis/survey/v2 v2.2.9/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
 github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go v56.3.0+incompatible h1:DmhwMrUIvpeoTDiWRDtNHqelNUd3Og8JCkrLHQK795c=
 github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go v63.4.0+incompatible h1:fle3M5Q7vr8auaiPffKyUQmLbvYeqpw30bKU6PrWJFo=
+github.com/Azure/azure-sdk-for-go v63.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.0 h1:D7l5jspkc4kwBYRWoZE4DQnu6LVpLwDsMZjBKS4wZLQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.0/go.mod h1:w5pDIZuawUmY3Bj4tVx3Xb8KS96ToB0j315w9rqpAg0=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1 h1:3CVsSo4mp8NDWO11tHzN/mdo2zP0CtaSK5IcwBjfqRA=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1/go.mod h1:w5pDIZuawUmY3Bj4tVx3Xb8KS96ToB0j315w9rqpAg0=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.14.0 h1:NVS/4LOQfkBpk+B1VopIzv1ptmYeEskA8w/3K/w7vjo=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.14.0/go.mod h1:RG0cZndeZM17StwohYclmcXSr4oOJ8b1I5hB8llIc6Y=
+github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 h1:sLZ/Y+P/5RRtsXWylBjB5lkgixYfm0MQPiwrSX//JSo=
+github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0 h1:vur33EIbAMNy6ClWXYHAcunXfxE4Ap6DElJQYa+eT78=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0/go.mod h1:+S1KCglmy9JANpBphmyhVMENwS8Bc4O40LwIGIaPoco=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@@ -90,6 +103,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
 github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c=
+github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
@@ -482,6 +497,7 @@ github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684 h1:DB
 github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac=
 github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
 github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/cli v20.10.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/cli v20.10.10+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
@@ -691,6 +707,8 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
+github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
@@ -1080,6 +1098,8 @@ github.com/kris-nova/logger v0.0.0-20181127235838-fd0d87064b06/go.mod h1:++9BgZu
 github.com/kris-nova/lolgopher v0.0.0-20180921204813-313b3abb0d9b h1:xYEM2oBUhBEhQjrV+KJ9lEWDWYZoNVZUaBF++Wyljq4=
 github.com/kris-nova/lolgopher v0.0.0-20180921204813-313b3abb0d9b/go.mod h1:V0HF/ZBlN86HqewcDC/cVxMmYDiRukWjSrgKLUAn9Js=
 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
@@ -1229,8 +1249,10 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
 github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
 github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
+github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
@@ -1339,6 +1361,8 @@ github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoU
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
+github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -1807,6 +1831,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=

+ 0 - 2
internal/kubernetes/prometheus/metrics.go

@@ -425,8 +425,6 @@ func createHPAAbsoluteMemoryThresholdQuery(memMetricName, metricName, podSelecti
 		kubeMetricsHPASelectorTwo,
 	)
 
-	fmt.Println("query is:")
-
 	return fmt.Sprintf(
 		`(%s * on(%s) %s) or (%s * on(%s) %s)`,
 		requestMemOne, hpaMetricName, targetMemUtilThresholdOne,

+ 56 - 0
internal/models/integrations/azure.go

@@ -0,0 +1,56 @@
+package integrations
+
+import (
+	"gorm.io/gorm"
+
+	"github.com/porter-dev/porter/api/types"
+)
+
+// AzureIntegration is an auth mechanism that uses a Azure service account principal to
+// authenticate
+type AzureIntegration struct {
+	gorm.Model
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The Azure client ID that this is linked to
+	AzureClientID string `json:"azure_client_id"`
+
+	// The Azure subscription ID that this is linked to
+	AzureSubscriptionID string `json:"azure_subscription_id"`
+
+	// The Azure tenant ID that this is linked to
+	AzureTenantID string `json:"azure_tenant_id"`
+
+	// ACR-specific fields
+	ACRTokenName         string `json:"acr_token_name"`
+	ACRResourceGroupName string `json:"acr_resource_group_name"`
+	ACRName              string `json:"acr_name"`
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// The Azure service principal key
+	ServicePrincipalSecret []byte `json:"service_principal_secret"`
+
+	// The ACR passwords, if set
+	ACRPassword1 []byte `json:"acr_password_1"`
+	ACRPassword2 []byte `json:"acr_password_2"`
+}
+
+func (a *AzureIntegration) ToAzureIntegrationType() *types.AzureIntegration {
+	return &types.AzureIntegration{
+		CreatedAt:           a.CreatedAt,
+		ID:                  a.ID,
+		UserID:              a.UserID,
+		ProjectID:           a.ProjectID,
+		AzureClientID:       a.AzureClientID,
+		AzureSubscriptionID: a.AzureSubscriptionID,
+		AzureTenantID:       a.AzureTenantID,
+	}
+}

+ 1 - 0
internal/models/project.go

@@ -55,6 +55,7 @@ type Project struct {
 	OAuthIntegrations []ints.OAuthIntegration `json:"oauth_integrations"`
 	AWSIntegrations   []ints.AWSIntegration   `json:"aws_integrations"`
 	GCPIntegrations   []ints.GCPIntegration   `json:"gcp_integrations"`
+	AzureIntegrations []ints.AzureIntegration `json:"azure_integrations"`
 
 	PreviewEnvsEnabled  bool
 	RDSDatabasesEnabled bool

+ 4 - 0
internal/models/registry.go

@@ -31,6 +31,7 @@ type Registry struct {
 
 	GCPIntegrationID   uint
 	AWSIntegrationID   uint
+	AzureIntegrationID uint
 	DOIntegrationID    uint
 	BasicIntegrationID uint
 
@@ -47,6 +48,8 @@ func (r *Registry) ToRegistryType() *types.Registry {
 		serv = types.GCR
 	} else if r.DOIntegrationID != 0 {
 		serv = types.DOCR
+	} else if r.AzureIntegrationID != 0 {
+		serv = types.ACR
 	} else if strings.Contains(r.URL, "index.docker.io") {
 		serv = types.DockerHub
 	}
@@ -67,6 +70,7 @@ func (r *Registry) ToRegistryType() *types.Registry {
 		InfraID:            r.InfraID,
 		GCPIntegrationID:   r.GCPIntegrationID,
 		AWSIntegrationID:   r.AWSIntegrationID,
+		AzureIntegrationID: r.AzureIntegrationID,
 		DOIntegrationID:    r.DOIntegrationID,
 		BasicIntegrationID: r.BasicIntegrationID,
 	}

+ 262 - 0
internal/registry/registry.go

@@ -10,6 +10,7 @@ import (
 	"strings"
 	"time"
 
+	"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"
 	"github.com/porter-dev/porter/internal/models"
@@ -24,6 +25,10 @@ import (
 	"github.com/digitalocean/godo"
 	"github.com/docker/cli/cli/config/configfile"
 	"github.com/docker/cli/cli/config/types"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
 )
 
 // Registry wraps the gorm Registry model
@@ -71,6 +76,10 @@ func (r *Registry) ListRepositories(
 		return r.listDOCRRepositories(repo, doAuth)
 	}
 
+	if r.AzureIntegrationID != 0 {
+		return r.listACRRepositories(repo)
+	}
+
 	if r.BasicIntegrationID != 0 {
 		return r.listPrivateRegistryRepositories(repo)
 	}
@@ -230,6 +239,174 @@ func (r *Registry) listECRRepositories(repo repository.Repository) ([]*ptypes.Re
 	return res, nil
 }
 
+func (r *Registry) listACRRepositories(repo repository.Repository) ([]*ptypes.RegistryRepository, error) {
+	az, err := repo.AzureIntegration().ReadAzureIntegration(
+		r.ProjectID,
+		r.AzureIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := &http.Client{}
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/v2/_catalog", r.URL),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req.SetBasicAuth(az.AzureClientID, string(az.ServicePrincipalSecret))
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	gcrResp := gcrRepositoryResp{}
+
+	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
+		return nil, fmt.Errorf("Could not read Azure registry repositories: %v", err)
+	}
+
+	res := make([]*ptypes.RegistryRepository, 0)
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, repo := range gcrResp.Repositories {
+		res = append(res, &ptypes.RegistryRepository{
+			Name: repo,
+			URI:  strings.TrimPrefix(r.URL, "https://") + "/" + repo,
+		})
+	}
+
+	return res, nil
+}
+
+// Returns the username/password pair for the registry
+func (r *Registry) GetACRCredentials(repo repository.Repository) (string, string, error) {
+	az, err := repo.AzureIntegration().ReadAzureIntegration(
+		r.ProjectID,
+		r.AzureIntegrationID,
+	)
+
+	if err != nil {
+		return "", "", err
+	}
+
+	// if the passwords and name aren't set, generate them
+	if az.ACRTokenName == "" || len(az.ACRPassword1) == 0 {
+		az.ACRTokenName = "porter-acr-token"
+
+		// create an acr repo token
+		cred, err := azidentity.NewClientSecretCredential(az.AzureTenantID, az.AzureClientID, string(az.ServicePrincipalSecret), nil)
+
+		if err != nil {
+			return "", "", err
+		}
+
+		scopeMapsClient, err := armcontainerregistry.NewScopeMapsClient(az.AzureSubscriptionID, cred, nil)
+
+		if err != nil {
+			return "", "", err
+		}
+
+		smRes, err := scopeMapsClient.Get(
+			context.Background(),
+			az.ACRResourceGroupName,
+			az.ACRName,
+			"_repositories_admin",
+			nil,
+		)
+
+		if err != nil {
+			return "", "", err
+		}
+
+		tokensClient, err := armcontainerregistry.NewTokensClient(az.AzureSubscriptionID, cred, nil)
+
+		if err != nil {
+			return "", "", err
+		}
+
+		pollerResp, err := tokensClient.BeginCreate(
+			context.Background(),
+			az.ACRResourceGroupName,
+			az.ACRName,
+			"porter-acr-token",
+			armcontainerregistry.Token{
+				Properties: &armcontainerregistry.TokenProperties{
+					ScopeMapID: smRes.ID,
+					Status:     to.Ptr(armcontainerregistry.TokenStatusEnabled),
+				},
+			},
+			nil,
+		)
+
+		if err != nil {
+			return "", "", err
+		}
+
+		tokResp, err := pollerResp.PollUntilDone(context.Background(), 2*time.Second)
+
+		if err != nil {
+			return "", "", err
+		}
+
+		registriesClient, err := armcontainerregistry.NewRegistriesClient(az.AzureSubscriptionID, cred, nil)
+
+		if err != nil {
+			return "", "", err
+		}
+
+		poller, err := registriesClient.BeginGenerateCredentials(
+			context.Background(),
+			az.ACRResourceGroupName,
+			az.ACRName,
+			armcontainerregistry.GenerateCredentialsParameters{
+				TokenID: tokResp.ID,
+			},
+			&armcontainerregistry.RegistriesClientBeginGenerateCredentialsOptions{ResumeToken: ""})
+
+		if err != nil {
+			return "", "", err
+		}
+
+		genCredentialsResp, err := poller.PollUntilDone(context.Background(), 2*time.Second)
+
+		if err != nil {
+			return "", "", err
+		}
+
+		for i, tokPassword := range genCredentialsResp.Passwords {
+			if i == 0 {
+				az.ACRPassword1 = []byte(*tokPassword.Value)
+			} else if i == 1 {
+				az.ACRPassword2 = []byte(*tokPassword.Value)
+			}
+		}
+
+		// update the az integration
+		az, err = repo.AzureIntegration().OverwriteAzureIntegration(
+			az,
+		)
+
+		if err != nil {
+			return "", "", err
+		}
+	}
+
+	return az.ACRTokenName, string(az.ACRPassword1), nil
+}
+
 func (r *Registry) listDOCRRepositories(
 	repo repository.Repository,
 	doAuth *oauth2.Config,
@@ -468,6 +645,10 @@ func (r *Registry) ListImages(
 		return r.listECRImages(repoName, repo)
 	}
 
+	if r.AzureIntegrationID != 0 {
+		return r.listACRImages(repoName, repo)
+	}
+
 	if r.GCPIntegrationID != 0 {
 		return r.listGCRImages(repoName, repo)
 	}
@@ -552,6 +733,55 @@ func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([
 	return res, nil
 }
 
+func (r *Registry) listACRImages(repoName string, repo repository.Repository) ([]*ptypes.Image, error) {
+	az, err := repo.AzureIntegration().ReadAzureIntegration(
+		r.ProjectID,
+		r.AzureIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// use JWT token to request catalog
+	client := &http.Client{}
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/v2/%s/tags/list", r.URL, repoName),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req.SetBasicAuth(az.AzureClientID, string(az.ServicePrincipalSecret))
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	gcrResp := gcrImageResp{}
+
+	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
+		return nil, fmt.Errorf("Could not read GCR repositories: %v", err)
+	}
+
+	res := make([]*ptypes.Image, 0)
+
+	for _, tag := range gcrResp.Tags {
+		res = append(res, &ptypes.Image{
+			RepositoryName: strings.TrimPrefix(repoName, "https://"),
+			Tag:            tag,
+		})
+	}
+
+	return res, nil
+}
+
 type gcrImageResp struct {
 	Tags []string `json:"tags"`
 }
@@ -845,6 +1075,10 @@ func (r *Registry) GetDockerConfigJSON(
 		conf, err = r.getPrivateRegistryDockerConfigFile(repo)
 	}
 
+	if r.AzureIntegrationID != 0 {
+		conf, err = r.getACRDockerConfigFile(repo)
+	}
+
 	if err != nil {
 		return nil, err
 	}
@@ -1015,6 +1249,34 @@ func (r *Registry) getPrivateRegistryDockerConfigFile(
 	}, nil
 }
 
+func (r *Registry) getACRDockerConfigFile(
+	repo repository.Repository,
+) (*configfile.ConfigFile, error) {
+	username, pw, err := r.GetACRCredentials(repo)
+
+	if err != nil {
+		return nil, err
+	}
+
+	key := r.URL
+
+	if !strings.Contains(key, "http") {
+		key = "https://" + key
+	}
+
+	parsedURL, _ := url.Parse(key)
+
+	return &configfile.ConfigFile{
+		AuthConfigs: map[string]types.AuthConfig{
+			parsedURL.Host: {
+				Username: string(username),
+				Password: string(pw),
+				Auth:     generateAuthToken(string(username), string(pw)),
+			},
+		},
+	}, nil
+}
+
 func generateAuthToken(username, password string) string {
 	return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
 }

+ 12 - 0
internal/repository/credentials/credentials.go

@@ -39,6 +39,15 @@ type AWSCredential struct {
 	AWSRegion []byte `json:"aws_region"`
 }
 
+type AzureCredential struct {
+	// The Azure service principal key
+	ServicePrincipalSecret []byte `json:"service_principal_secret"`
+
+	// The ACR passwords, if set
+	ACRPassword1 []byte `json:"acr_password_1"`
+	ACRPassword2 []byte `json:"acr_password_2"`
+}
+
 type CredentialStorage interface {
 	WriteOAuthCredential(oauthIntegration *integrations.OAuthIntegration, data *OAuthCredential) error
 	GetOAuthCredential(oauthIntegration *integrations.OAuthIntegration) (*OAuthCredential, error)
@@ -49,4 +58,7 @@ type CredentialStorage interface {
 	WriteAWSCredential(awsIntegration *integrations.AWSIntegration, data *AWSCredential) error
 	GetAWSCredential(awsIntegration *integrations.AWSIntegration) (*AWSCredential, error)
 	CreateAWSToken(awsIntegration *integrations.AWSIntegration) (string, error)
+	WriteAzureCredential(azIntegration *integrations.AzureIntegration, data *AzureCredential) error
+	GetAzureCredential(azIntegration *integrations.AzureIntegration) (*AzureCredential, error)
+	CreateAzureToken(azIntegration *integrations.AzureIntegration) (string, error)
 }

+ 230 - 0
internal/repository/gorm/auth.go

@@ -1315,3 +1315,233 @@ func (repo *GithubAppOAuthIntegrationRepository) UpdateGithubAppOauthIntegration
 
 	return am, nil
 }
+
+// AzureIntegrationRepository uses gorm.DB for querying the database
+type AzureIntegrationRepository struct {
+	db             *gorm.DB
+	key            *[32]byte
+	storageBackend credentials.CredentialStorage
+}
+
+// NewAzureIntegrationRepository returns a AzureIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewAzureIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+	storageBackend credentials.CredentialStorage,
+) repository.AzureIntegrationRepository {
+	return &AzureIntegrationRepository{db, key, storageBackend}
+}
+
+// CreateAzureIntegration creates a new Azure auth mechanism
+func (repo *AzureIntegrationRepository) CreateAzureIntegration(
+	az *ints.AzureIntegration,
+) (*ints.AzureIntegration, error) {
+	err := repo.EncryptAzureIntegrationData(az, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// if storage backend is not nil, strip out credential data, which will be stored in credential
+	// storage backend after write to DB
+	var credentialData = &credentials.AzureCredential{}
+
+	if repo.storageBackend != nil {
+		credentialData.ServicePrincipalSecret = az.ServicePrincipalSecret
+		credentialData.ACRPassword1 = az.ACRPassword1
+		credentialData.ACRPassword2 = az.ACRPassword2
+		az.ServicePrincipalSecret = []byte{}
+		az.ACRPassword1 = []byte{}
+		az.ACRPassword2 = []byte{}
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", az.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("AzureIntegrations")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(az); err != nil {
+		return nil, err
+	}
+
+	if repo.storageBackend != nil {
+		err = repo.storageBackend.WriteAzureCredential(az, credentialData)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return az, nil
+}
+
+// OverwriteAzureIntegration overwrites the Azure credential in the DB
+func (repo *AzureIntegrationRepository) OverwriteAzureIntegration(
+	az *ints.AzureIntegration,
+) (*ints.AzureIntegration, error) {
+	err := repo.EncryptAzureIntegrationData(az, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// if storage backend is not nil, strip out credential data, which will be stored in credential
+	// storage backend after write to DB
+	var credentialData = &credentials.AzureCredential{}
+
+	if repo.storageBackend != nil {
+		credentialData.ServicePrincipalSecret = az.ServicePrincipalSecret
+		credentialData.ACRPassword1 = az.ACRPassword1
+		credentialData.ACRPassword2 = az.ACRPassword2
+		az.ServicePrincipalSecret = []byte{}
+		az.ACRPassword1 = []byte{}
+		az.ACRPassword2 = []byte{}
+	}
+
+	if err := repo.db.Save(az).Error; err != nil {
+		return nil, err
+	}
+
+	if repo.storageBackend != nil {
+		err = repo.storageBackend.WriteAzureCredential(az, credentialData)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// perform another read
+	return repo.ReadAzureIntegration(az.ProjectID, az.ID)
+}
+
+// ReadAzureIntegration finds a Azure auth mechanism by id
+func (repo *AzureIntegrationRepository) ReadAzureIntegration(
+	projectID, id uint,
+) (*ints.AzureIntegration, error) {
+	az := &ints.AzureIntegration{}
+
+	if err := repo.db.Where("project_id = ? AND id = ?", projectID, id).First(&az).Error; err != nil {
+		return nil, err
+	}
+
+	if repo.storageBackend != nil {
+		credentialData, err := repo.storageBackend.GetAzureCredential(az)
+
+		if err != nil {
+			return nil, err
+		}
+
+		az.ServicePrincipalSecret = credentialData.ServicePrincipalSecret
+		az.ACRPassword1 = credentialData.ACRPassword1
+		az.ACRPassword2 = credentialData.ACRPassword2
+	}
+
+	err := repo.DecryptAzureIntegrationData(az, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return az, nil
+}
+
+// ListAzureIntegrationsByProjectID finds all Azure auth mechanisms
+// for a given project id
+func (repo *AzureIntegrationRepository) ListAzureIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.AzureIntegration, error) {
+	azs := []*ints.AzureIntegration{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&azs).Error; err != nil {
+		return nil, err
+	}
+
+	return azs, nil
+}
+
+// EncryptAWSIntegrationData will encrypt the aws integration data before
+// writing to the DB
+func (repo *AzureIntegrationRepository) EncryptAzureIntegrationData(
+	az *ints.AzureIntegration,
+	key *[32]byte,
+) error {
+	if len(az.ServicePrincipalSecret) > 0 {
+		cipherData, err := encryption.Encrypt(az.ServicePrincipalSecret, key)
+
+		if err != nil {
+			return err
+		}
+
+		az.ServicePrincipalSecret = cipherData
+	}
+
+	if len(az.ACRPassword1) > 0 {
+		cipherData, err := encryption.Encrypt(az.ACRPassword1, key)
+
+		if err != nil {
+			return err
+		}
+
+		az.ACRPassword1 = cipherData
+	}
+
+	if len(az.ACRPassword2) > 0 {
+		cipherData, err := encryption.Encrypt(az.ACRPassword2, key)
+
+		if err != nil {
+			return err
+		}
+
+		az.ACRPassword2 = cipherData
+	}
+
+	return nil
+}
+
+// DecryptAzureIntegrationData will decrypt the Azure integration data before
+// returning it from the DB
+func (repo *AzureIntegrationRepository) DecryptAzureIntegrationData(
+	az *ints.AzureIntegration,
+	key *[32]byte,
+) error {
+	if len(az.ServicePrincipalSecret) > 0 {
+		plaintext, err := encryption.Decrypt(az.ServicePrincipalSecret, key)
+
+		if err != nil {
+			return err
+		}
+
+		az.ServicePrincipalSecret = plaintext
+	}
+
+	if len(az.ACRPassword1) > 0 {
+		plaintext, err := encryption.Decrypt(az.ACRPassword1, key)
+
+		if err != nil {
+			return err
+		}
+
+		az.ACRPassword1 = plaintext
+	}
+
+	if len(az.ACRPassword2) > 0 {
+		plaintext, err := encryption.Decrypt(az.ACRPassword2, key)
+
+		if err != nil {
+			return err
+		}
+
+		az.ACRPassword2 = plaintext
+	}
+
+	return nil
+}

+ 1 - 0
internal/repository/gorm/migrate.go

@@ -54,6 +54,7 @@ func AutoMigrate(db *gorm.DB, debug bool) error {
 		&ints.OAuthIntegration{},
 		&ints.GCPIntegration{},
 		&ints.AWSIntegration{},
+		&ints.AzureIntegration{},
 		&ints.TokenCache{},
 		&ints.ClusterTokenCache{},
 		&ints.RegTokenCache{},

+ 6 - 0
internal/repository/gorm/repository.go

@@ -29,6 +29,7 @@ type GormRepository struct {
 	oauthIntegration          repository.OAuthIntegrationRepository
 	gcpIntegration            repository.GCPIntegrationRepository
 	awsIntegration            repository.AWSIntegrationRepository
+	azIntegration             repository.AzureIntegrationRepository
 	githubAppInstallation     repository.GithubAppInstallationRepository
 	githubAppOAuthIntegration repository.GithubAppOAuthIntegrationRepository
 	slackIntegration          repository.SlackIntegrationRepository
@@ -131,6 +132,10 @@ func (t *GormRepository) AWSIntegration() repository.AWSIntegrationRepository {
 	return t.awsIntegration
 }
 
+func (t *GormRepository) AzureIntegration() repository.AzureIntegrationRepository {
+	return t.azIntegration
+}
+
 func (t *GormRepository) GithubAppInstallation() repository.GithubAppInstallationRepository {
 	return t.githubAppInstallation
 }
@@ -205,6 +210,7 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		oauthIntegration:          NewOAuthIntegrationRepository(db, key, storageBackend),
 		gcpIntegration:            NewGCPIntegrationRepository(db, key, storageBackend),
 		awsIntegration:            NewAWSIntegrationRepository(db, key, storageBackend),
+		azIntegration:             NewAzureIntegrationRepository(db, key, storageBackend),
 		githubAppInstallation:     NewGithubAppInstallationRepository(db),
 		githubAppOAuthIntegration: NewGithubAppOAuthIntegrationRepository(db),
 		slackIntegration:          NewSlackIntegrationRepository(db, key),

+ 9 - 0
internal/repository/integrations.go

@@ -61,6 +61,15 @@ type AWSIntegrationRepository interface {
 	ListAWSIntegrationsByProjectID(projectID uint) ([]*ints.AWSIntegration, error)
 }
 
+// AzureIntegrationRepository represents the set of queries on the AWS auth
+// mechanism
+type AzureIntegrationRepository interface {
+	CreateAzureIntegration(az *ints.AzureIntegration) (*ints.AzureIntegration, error)
+	OverwriteAzureIntegration(az *ints.AzureIntegration) (*ints.AzureIntegration, error)
+	ReadAzureIntegration(projectID, id uint) (*ints.AzureIntegration, error)
+	ListAzureIntegrationsByProjectID(projectID uint) ([]*ints.AzureIntegration, error)
+}
+
 // GCPIntegrationRepository represents the set of queries on the GCP auth
 // mechanism
 type GCPIntegrationRepository interface {

+ 1 - 0
internal/repository/repository.go

@@ -23,6 +23,7 @@ type Repository interface {
 	OAuthIntegration() OAuthIntegrationRepository
 	GCPIntegration() GCPIntegrationRepository
 	AWSIntegration() AWSIntegrationRepository
+	AzureIntegration() AzureIntegrationRepository
 	GithubAppInstallation() GithubAppInstallationRepository
 	GithubAppOAuthIntegration() GithubAppOAuthIntegrationRepository
 	SlackIntegration() SlackIntegrationRepository

+ 40 - 0
internal/repository/test/auth.go

@@ -566,3 +566,43 @@ func (repo *GithubAppOAuthIntegrationRepository) UpdateGithubAppOauthIntegration
 
 	return am, nil
 }
+
+// AzureIntegrationRepository (unimplemented)
+type AzureIntegrationRepository struct {
+}
+
+// NewAzureIntegrationRepository returns a AzureIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewAzureIntegrationRepository() repository.AzureIntegrationRepository {
+	return &AzureIntegrationRepository{}
+}
+
+// CreateAzureIntegration creates a new Azure auth mechanism
+func (repo *AzureIntegrationRepository) CreateAzureIntegration(
+	az *ints.AzureIntegration,
+) (*ints.AzureIntegration, error) {
+	panic("unimplemented")
+}
+
+// OverwriteAzureIntegration overwrites the Azure credential in the DB
+func (repo *AzureIntegrationRepository) OverwriteAzureIntegration(
+	az *ints.AzureIntegration,
+) (*ints.AzureIntegration, error) {
+	panic("unimplemented")
+}
+
+// ReadAzureIntegration finds a Azure auth mechanism by id
+func (repo *AzureIntegrationRepository) ReadAzureIntegration(
+	projectID, id uint,
+) (*ints.AzureIntegration, error) {
+	panic("unimplemented")
+}
+
+// ListAzureIntegrationsByProjectID finds all Azure auth mechanisms
+// for a given project id
+func (repo *AzureIntegrationRepository) ListAzureIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.AzureIntegration, error) {
+	panic("unimplemented")
+}

+ 6 - 0
internal/repository/test/repository.go

@@ -26,6 +26,7 @@ type TestRepository struct {
 	oauthIntegration          repository.OAuthIntegrationRepository
 	gcpIntegration            repository.GCPIntegrationRepository
 	awsIntegration            repository.AWSIntegrationRepository
+	azIntegration             repository.AzureIntegrationRepository
 	githubAppInstallation     repository.GithubAppInstallationRepository
 	githubAppOAuthIntegration repository.GithubAppOAuthIntegrationRepository
 	slackIntegration          repository.SlackIntegrationRepository
@@ -125,6 +126,10 @@ func (t *TestRepository) AWSIntegration() repository.AWSIntegrationRepository {
 	return t.awsIntegration
 }
 
+func (t *TestRepository) AzureIntegration() repository.AzureIntegrationRepository {
+	return t.azIntegration
+}
+
 func (t *TestRepository) GithubAppInstallation() repository.GithubAppInstallationRepository {
 	return t.githubAppInstallation
 }
@@ -202,6 +207,7 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		oauthIntegration:          NewOAuthIntegrationRepository(canQuery),
 		gcpIntegration:            NewGCPIntegrationRepository(canQuery),
 		awsIntegration:            NewAWSIntegrationRepository(canQuery),
+		azIntegration:             NewAzureIntegrationRepository(),
 		githubAppInstallation:     NewGithubAppInstallationRepository(canQuery),
 		githubAppOAuthIntegration: NewGithubAppOAuthIntegrationRepository(canQuery),
 		slackIntegration:          NewSlackIntegrationRepository(canQuery),

+ 1 - 1
services/cli_install_script_container/Dockerfile

@@ -1,4 +1,4 @@
-FROM golang:1.17.6-alpine3.14
+FROM golang:1.18-alpine
 
 WORKDIR /app
 COPY . .

+ 1 - 1
services/porter_cli_container/dev.Dockerfile

@@ -2,7 +2,7 @@
 
 # Base Go environment
 # -------------------
-FROM golang:1.17 as base
+FROM golang:1.18 as base
 WORKDIR /porter
 
 RUN apt-get update && apt-get install -y gcc musl-dev git

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff