Kaynağa Gözat

add support for azure container registry

Alexander Belanger 4 yıl önce
ebeveyn
işleme
9dd9c64953
36 değiştirilmiş dosya ile 994 ekleme ve 14 silme
  1. 2 2
      .github/workflows/prerelease.yaml
  2. 19 0
      api/client/registry.go
  3. 66 0
      api/server/handlers/project_integration/create_azure.go
  4. 1 0
      api/server/handlers/registry/create.go
  5. 54 0
      api/server/handlers/registry/get_token.go
  6. 28 0
      api/server/router/project.go
  7. 28 0
      api/server/router/project_integration.go
  8. 32 0
      api/types/project_integration.go
  9. 5 0
      api/types/registry.go
  10. 1 1
      build/Dockerfile.osx
  11. 1 1
      build/Dockerfile.win
  12. 30 0
      cli/cmd/docker/auth.go
  13. 1 1
      docker/Dockerfile
  14. 1 1
      docker/cli.Dockerfile
  15. 1 1
      docker/dev.Dockerfile
  16. 1 1
      ee/docker/ee.Dockerfile
  17. 1 1
      ee/docker/provisioner.Dockerfile
  18. 10 0
      ee/integrations/vault/types.go
  19. 38 0
      ee/integrations/vault/vault.go
  20. 13 1
      go.mod
  21. 25 0
      go.sum
  22. 0 2
      internal/kubernetes/prometheus/metrics.go
  23. 56 0
      internal/models/integrations/azure.go
  24. 1 0
      internal/models/project.go
  25. 4 0
      internal/models/registry.go
  26. 263 0
      internal/registry/registry.go
  27. 12 0
      internal/repository/credentials/credentials.go
  28. 235 0
      internal/repository/gorm/auth.go
  29. 1 0
      internal/repository/gorm/migrate.go
  30. 6 0
      internal/repository/gorm/repository.go
  31. 9 0
      internal/repository/integrations.go
  32. 1 0
      internal/repository/repository.go
  33. 40 0
      internal/repository/test/auth.go
  34. 6 0
      internal/repository/test/repository.go
  35. 1 1
      services/cli_install_script_container/Dockerfile
  36. 1 1
      services/porter_cli_container/dev.Dockerfile

+ 2 - 2
.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

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

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

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

@@ -57,6 +57,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 {

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

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

+ 5 - 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,7 @@ 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"`
 }
 
 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}
 

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

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

+ 263 - 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,175 @@ 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
+		}
+
+		// TODO: CALL GENERATE CREDENTIALS ENDPOINT
+		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 +646,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 +734,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 +1076,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 +1250,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)
 }

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

@@ -1315,3 +1315,238 @@ 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
+		}
+	}
+
+	err = repo.DecryptAzureIntegrationData(az, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return az, nil
+}
+
+// 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