Bläddra i källkod

add support for azure forms and aks provisioner

Alexander Belanger 4 år sedan
förälder
incheckning
fb363f44bf

+ 19 - 1
api/server/handlers/infra/create.go

@@ -148,6 +148,7 @@ func checkInfraCredentials(config *config.Config, proj *models.Project, infra *m
 		infra.DOIntegrationID = req.DOIntegrationID
 		infra.DOIntegrationID = req.DOIntegrationID
 		infra.AWSIntegrationID = 0
 		infra.AWSIntegrationID = 0
 		infra.GCPIntegrationID = 0
 		infra.GCPIntegrationID = 0
+		infra.AzureIntegrationID = 0
 	} else if req.AWSIntegrationID != 0 {
 	} else if req.AWSIntegrationID != 0 {
 		_, err := config.Repo.AWSIntegration().ReadAWSIntegration(proj.ID, req.AWSIntegrationID)
 		_, err := config.Repo.AWSIntegration().ReadAWSIntegration(proj.ID, req.AWSIntegrationID)
 
 
@@ -158,6 +159,7 @@ func checkInfraCredentials(config *config.Config, proj *models.Project, infra *m
 		infra.DOIntegrationID = 0
 		infra.DOIntegrationID = 0
 		infra.AWSIntegrationID = req.AWSIntegrationID
 		infra.AWSIntegrationID = req.AWSIntegrationID
 		infra.GCPIntegrationID = 0
 		infra.GCPIntegrationID = 0
+		infra.AzureIntegrationID = 0
 	} else if req.GCPIntegrationID != 0 {
 	} else if req.GCPIntegrationID != 0 {
 		_, err := config.Repo.GCPIntegration().ReadGCPIntegration(proj.ID, req.GCPIntegrationID)
 		_, err := config.Repo.GCPIntegration().ReadGCPIntegration(proj.ID, req.GCPIntegrationID)
 
 
@@ -168,9 +170,21 @@ func checkInfraCredentials(config *config.Config, proj *models.Project, infra *m
 		infra.DOIntegrationID = 0
 		infra.DOIntegrationID = 0
 		infra.AWSIntegrationID = 0
 		infra.AWSIntegrationID = 0
 		infra.GCPIntegrationID = req.GCPIntegrationID
 		infra.GCPIntegrationID = req.GCPIntegrationID
+		infra.AzureIntegrationID = 0
+	} else if req.AzureIntegrationID != 0 {
+		_, err := config.Repo.AzureIntegration().ReadAzureIntegration(proj.ID, req.AzureIntegrationID)
+
+		if err != nil {
+			return fmt.Errorf("azure integration id %d not found in project %d", req.AzureIntegrationID, proj.ID)
+		}
+
+		infra.DOIntegrationID = 0
+		infra.AWSIntegrationID = 0
+		infra.GCPIntegrationID = 0
+		infra.AzureIntegrationID = req.AzureIntegrationID
 	}
 	}
 
 
-	if infra.DOIntegrationID == 0 && infra.AWSIntegrationID == 0 && infra.GCPIntegrationID == 0 {
+	if infra.DOIntegrationID == 0 && infra.AWSIntegrationID == 0 && infra.GCPIntegrationID == 0 && infra.AzureIntegrationID == 0 {
 		return fmt.Errorf("at least one integration id must be set")
 		return fmt.Errorf("at least one integration id must be set")
 	}
 	}
 
 
@@ -195,6 +209,10 @@ func getSourceLinkAndVersion(kind types.InfraKind) (string, string) {
 		return "porter/do/docr", "v0.1.0"
 		return "porter/do/docr", "v0.1.0"
 	case types.InfraDOKS:
 	case types.InfraDOKS:
 		return "porter/do/doks", "v0.1.0"
 		return "porter/do/doks", "v0.1.0"
+	case types.InfraAKS:
+		return "porter/azure/aks", "v0.1.0"
+	case types.InfraACR:
+		return "porter/azure/acr", "v0.1.0"
 	}
 	}
 
 
 	return "porter/test", "v0.1.0"
 	return "porter/test", "v0.1.0"

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

@@ -748,3 +748,78 @@ tabs:
       placeholder: my-cluster
       placeholder: my-cluster
       variable: cluster_name
       variable: cluster_name
 `
 `
+
+const acrForm = `name: ACR
+hasSource: false
+includeHiddenFields: true
+isClusterScoped: false
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: ACR Configuration
+    - type: select
+      label: 📍 Azure Region
+      variable: aks_region
+      settings:
+        default: East US
+        options:
+        - label: East US
+          value: East US
+        - label: East US 2
+          value: East US 2
+        - label: West US 2
+          value: West US 2
+        - label: West US 3
+          value: West US 3
+        - label: Norway East
+          value: Norway East
+    - type: string-input
+      label: ACR Name
+      required: true
+      placeholder: my-registry
+      variable: acr_name
+`
+
+const aksForm = `name: AKS
+hasSource: false
+includeHiddenFields: true
+isClusterScoped: false
+tabs:
+- name: main
+  label: Configuration
+  sections:
+  - name: section_one
+    contents: 
+    - type: heading
+      label: AKS Configuration
+    - type: select
+      label: 📍 Azure Region
+      variable: aks_region
+      settings:
+        default: East US
+        options:
+        - label: East US
+          value: East US
+        - label: East US 2
+          value: East US 2
+        - label: West US 2
+          value: West US 2
+        - label: West US 3
+          value: West US 3
+        - label: Norway East
+          value: Norway East
+    - type: string-input
+      label: 👤 Issuer Email
+      required: true
+      placeholder: example@example.com
+      variable: issuer_email
+    - type: string-input
+      label: AKS Cluster Name
+      required: true
+      placeholder: my-cluster
+      variable: cluster_name
+`

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

@@ -77,6 +77,10 @@ func getFormBytesFromKind(kind string) []byte {
 		formBytes = []byte(docrForm)
 		formBytes = []byte(docrForm)
 	case "doks":
 	case "doks":
 		formBytes = []byte(doksForm)
 		formBytes = []byte(doksForm)
+	case "aks":
+		formBytes = []byte(aksForm)
+	case "acr":
+		formBytes = []byte(acrForm)
 	}
 	}
 
 
 	return formBytes
 	return formBytes

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

@@ -97,4 +97,20 @@ var templateMap = map[string]*types.InfraTemplateMeta{
 		Kind:               "doks",
 		Kind:               "doks",
 		RequiredCredential: "do_integration_id",
 		RequiredCredential: "do_integration_id",
 	},
 	},
+	"acr": {
+		Icon:               "",
+		Description:        "Create an Azure Container Registry.",
+		Name:               "ACR",
+		Version:            "v0.1.0",
+		Kind:               "acr",
+		RequiredCredential: "azure_integration_id",
+	},
+	"aks": {
+		Icon:               "",
+		Description:        "Create an Azure Kubernetes Service cluster",
+		Name:               "AKS",
+		Version:            "v0.1.0",
+		Kind:               "aks",
+		RequiredCredential: "azure_integration_id",
+	},
 }
 }

+ 44 - 0
api/server/handlers/project_integration/list_azure.go

@@ -0,0 +1,44 @@
+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"
+)
+
+type ListAzureHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewListAzureHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListAzureHandler {
+	return &ListAzureHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ListAzureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	azInts, err := p.Repo().AzureIntegration().ListAzureIntegrationsByProjectID(project.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.ListAzureResponse = make([]*types.AzureIntegration, 0)
+
+	for _, azInt := range azInts {
+		res = append(res, azInt.ToAzureIntegrationType())
+	}
+
+	p.WriteResult(w, r, res)
+}

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

@@ -217,6 +217,33 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/integrations/azure -> project_integration.NewListAzureHandler
+	listAzureEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/azure",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listAzureHandler := project_integration.NewListAzureHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listAzureEndpoint,
+		Handler:  listAzureHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/integrations/gcp -> project_integration.NewCreateGCPHandler
 	// POST /api/projects/{project_id}/integrations/gcp -> project_integration.NewCreateGCPHandler
 	createGCPEndpoint := factory.NewAPIEndpoint(
 	createGCPEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{

+ 1 - 0
api/types/cluster.go

@@ -167,6 +167,7 @@ const (
 	DOKS ClusterService = "doks"
 	DOKS ClusterService = "doks"
 	GKE  ClusterService = "gke"
 	GKE  ClusterService = "gke"
 	Kube ClusterService = "kube"
 	Kube ClusterService = "kube"
+	AKS  ClusterService = "aks"
 )
 )
 
 
 // ClusterResolverName is the name for a cluster resolve
 // ClusterResolverName is the name for a cluster resolve

+ 9 - 3
api/types/infra.go

@@ -26,6 +26,8 @@ const (
 	InfraGKE  InfraKind = "gke"
 	InfraGKE  InfraKind = "gke"
 	InfraDOCR InfraKind = "docr"
 	InfraDOCR InfraKind = "docr"
 	InfraDOKS InfraKind = "doks"
 	InfraDOKS InfraKind = "doks"
+	InfraAKS  InfraKind = "aks"
+	InfraACR  InfraKind = "acr"
 
 
 	InfraRDS InfraKind = "rds"
 	InfraRDS InfraKind = "rds"
 )
 )
@@ -59,6 +61,9 @@ type Infra struct {
 	// this points to an OAuthIntegrationID
 	// this points to an OAuthIntegrationID
 	DOIntegrationID uint `json:"do_integration_id,omitempty"`
 	DOIntegrationID uint `json:"do_integration_id,omitempty"`
 
 
+	// The Azure integration that was used to create the infra
+	AzureIntegrationID uint `json:"azure_integration_id,omitempty"`
+
 	// The last-applied, non-sensitive input variables to the provisioner. For now,
 	// The last-applied, non-sensitive input variables to the provisioner. For now,
 	// this is a map[string]string since we marshal into env vars anyway, but
 	// this is a map[string]string since we marshal into env vars anyway, but
 	// eventually this config will be more complex.
 	// eventually this config will be more complex.
@@ -70,9 +75,10 @@ type Infra struct {
 }
 }
 
 
 type InfraCredentials struct {
 type InfraCredentials struct {
-	AWSIntegrationID uint `json:"aws_integration_id,omitempty"`
-	GCPIntegrationID uint `json:"gcp_integration_id,omitempty"`
-	DOIntegrationID  uint `json:"do_integration_id,omitempty"`
+	AWSIntegrationID   uint `json:"aws_integration_id,omitempty"`
+	GCPIntegrationID   uint `json:"gcp_integration_id,omitempty"`
+	DOIntegrationID    uint `json:"do_integration_id,omitempty"`
+	AzureIntegrationID uint `json:"azure_integration_id,omitempty"`
 }
 }
 
 
 type CreateInfraRequest struct {
 type CreateInfraRequest struct {

+ 2 - 0
api/types/project_integration.go

@@ -157,3 +157,5 @@ type CreateAzureRequest struct {
 type CreateAzureResponse struct {
 type CreateAzureResponse struct {
 	*AzureIntegration
 	*AzureIntegration
 }
 }
+
+type ListAzureResponse []*AzureIntegration

+ 17 - 0
dashboard/src/main/home/infrastructure/components/ProvisionInfra.tsx

@@ -25,6 +25,7 @@ import Select from "components/porter-form/field-components/Select";
 import ClusterList from "./credentials/ClusterList";
 import ClusterList from "./credentials/ClusterList";
 import { useLocation, useParams } from "react-router";
 import { useLocation, useParams } from "react-router";
 import qs from "qs";
 import qs from "qs";
+import AzureCredentialsList from "./credentials/AzureCredentialList";
 
 
 type Props = {};
 type Props = {};
 
 
@@ -128,6 +129,7 @@ const ProvisionInfra: React.FunctionComponent<Props> = () => {
           aws_integration_id: currentCredential["aws_integration_id"],
           aws_integration_id: currentCredential["aws_integration_id"],
           do_integration_id: currentCredential["do_integration_id"],
           do_integration_id: currentCredential["do_integration_id"],
           gcp_integration_id: currentCredential["gcp_integration_id"],
           gcp_integration_id: currentCredential["gcp_integration_id"],
+          azure_integration_id: currentCredential["azure_integration_id"],
           cluster_id: selectedClusterID || null,
           cluster_id: selectedClusterID || null,
         },
         },
         {
         {
@@ -248,6 +250,21 @@ const ProvisionInfra: React.FunctionComponent<Props> = () => {
             />
             />
           </ActionContainer>
           </ActionContainer>
         );
         );
+      } else if (
+        currentTemplate.required_credential == "azure_integration_id"
+      ) {
+        return (
+          <ActionContainer>
+            <Heading>Step 1 of {numSteps} - Link Azure Credentials</Heading>
+            <AzureCredentialsList
+              selectCredential={(i) =>
+                setCurrentCredential({
+                  azure_integration_id: i,
+                })
+              }
+            />
+          </ActionContainer>
+        );
       }
       }
     }
     }
 
 

+ 110 - 0
dashboard/src/main/home/infrastructure/components/credentials/AzureCredentialList.tsx

@@ -0,0 +1,110 @@
+import React, { useContext, useEffect, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+import Loading from "components/Loading";
+import Placeholder from "components/Placeholder";
+// import AzureCredentialForm from "./AzureCredentialForm";
+import CredentialList from "./CredentialList";
+import Description from "components/Description";
+
+type Props = {
+  selectCredential: (azure_integration_id: number) => void;
+};
+
+type AzureCredential = {
+  created_at: string;
+  id: number;
+  user_id: number;
+  project_id: number;
+  azure_client_id: string;
+};
+
+const AzureCredentialsList: React.FunctionComponent<Props> = ({
+  selectCredential,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [isLoading, setIsLoading] = useState(true);
+  const [azCredentials, setAzureCredentials] = useState<AzureCredential[]>(
+    null
+  );
+  const [shouldCreateCred, setShouldCreateCred] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  useEffect(() => {
+    api
+      .getAzureIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        if (!Array.isArray(data)) {
+          throw Error("Data is not an array");
+        }
+
+        setAzureCredentials(data);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  }, [currentProject]);
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  const renderContents = () => {
+    // if (shouldCreateCred) {
+    //   return (
+    //     <AzureCredentialForm
+    //       setCreatedCredential={selectCredential}
+    //       cancel={() => {}}
+    //     />
+    //   );
+    // }
+
+    return (
+      <>
+        <Description>
+          Select your credentials from the list below, or create a new
+          credential:
+        </Description>
+        <CredentialList
+          credentials={azCredentials.map((cred) => {
+            return {
+              id: cred.id,
+              display_name: cred.azure_client_id,
+              created_at: cred.created_at,
+            };
+          })}
+          selectCredential={selectCredential}
+          shouldCreateCred={() => setShouldCreateCred(true)}
+          addNewText="Add New Azure Credential"
+        />
+      </>
+    );
+  };
+
+  return <AzureCredentialWrapper>{renderContents()}</AzureCredentialWrapper>;
+};
+
+export default AzureCredentialsList;
+
+const AzureCredentialWrapper = styled.div`
+  margin-top: 20px;
+`;

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

@@ -56,6 +56,11 @@ const getGCPIntegration = baseApi<{}, { project_id: number }>(
   ({ project_id }) => `/api/projects/${project_id}/integrations/gcp`
   ({ project_id }) => `/api/projects/${project_id}/integrations/gcp`
 );
 );
 
 
+const getAzureIntegration = baseApi<{}, { project_id: number }>(
+  "GET",
+  ({ project_id }) => `/api/projects/${project_id}/integrations/azure`
+);
+
 const createAWSIntegration = baseApi<
 const createAWSIntegration = baseApi<
   {
   {
     aws_region: string;
     aws_region: string;
@@ -709,6 +714,7 @@ const provisionInfra = baseApi<
     aws_integration_id?: number;
     aws_integration_id?: number;
     gcp_integration_id?: number;
     gcp_integration_id?: number;
     do_integration_id?: number;
     do_integration_id?: number;
+    azure_integration_id?: number;
     cluster_id?: number;
     cluster_id?: number;
   },
   },
   {
   {
@@ -1787,6 +1793,7 @@ export default {
   connectDORegistry,
   connectDORegistry,
   getAWSIntegration,
   getAWSIntegration,
   getGCPIntegration,
   getGCPIntegration,
+  getAzureIntegration,
   createAWSIntegration,
   createAWSIntegration,
   overwriteAWSIntegration,
   overwriteAWSIntegration,
   createEmailVerification,
   createEmailVerification,

+ 12 - 1
dashboard/src/shared/common.tsx

@@ -25,7 +25,8 @@ export const integrationList: any = {
     buttonText: "Link a Github Account",
     buttonText: "Link a Github Account",
   },
   },
   slack: {
   slack: {
-    icon: "https://user-images.githubusercontent.com/5147537/54070671-0a173780-4263-11e9-8946-09ac0e37d8c6.png",
+    icon:
+      "https://user-images.githubusercontent.com/5147537/54070671-0a173780-4263-11e9-8946-09ac0e37d8c6.png",
     label: "Slack",
     label: "Slack",
     buttonText: "Install Application",
     buttonText: "Install Application",
   },
   },
@@ -78,6 +79,16 @@ export const integrationList: any = {
       "https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png",
       "https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png",
     label: "Digital Ocean Container Registry (DOCR)",
     label: "Digital Ocean Container Registry (DOCR)",
   },
   },
+  aks: {
+    icon:
+      "https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png",
+    label: "Azure Kubernetes Service (AKS)",
+  },
+  acr: {
+    icon:
+      "https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png",
+    label: "Azure Container Registry (ACR)",
+  },
   aws: {
   aws: {
     icon: aws,
     icon: aws,
     label: "AWS",
     label: "AWS",

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

@@ -381,6 +381,8 @@ export type InfraKind =
   | "gcr"
   | "gcr"
   | "doks"
   | "doks"
   | "docr"
   | "docr"
+  | "aks"
+  | "acr"
   | "test";
   | "test";
 
 
 export type OperationStatus = "starting" | "completed" | "errored";
 export type OperationStatus = "starting" | "completed" | "errored";
@@ -508,6 +510,20 @@ export const KindMap: ProviderInfoMap = {
     resource_link: "/dashboard",
     resource_link: "/dashboard",
     provider_name: "Digital Ocean Kubernetes Service (DOKS)",
     provider_name: "Digital Ocean Kubernetes Service (DOKS)",
   },
   },
+  aks: {
+    provider: "azure",
+    source: "porter/azure/aks",
+    resource_name: "Cluster",
+    resource_link: "/dashboard",
+    provider_name: "Azure Kubernetes Service (AKS)",
+  },
+  acr: {
+    provider: "azure",
+    source: "porter/azure/acr",
+    resource_name: "Registry",
+    resource_link: "/integrations/registry",
+    provider_name: "Azure Container Registry (ACR)",
+  },
   test: {
   test: {
     provider: "aws",
     provider: "aws",
     source: "porter/test",
     source: "porter/test",
@@ -540,6 +556,7 @@ export type InfraCredentialOptions =
   | "aws_integration_id"
   | "aws_integration_id"
   | "gcp_integration_id"
   | "gcp_integration_id"
   | "do_integration_id"
   | "do_integration_id"
+  | "azure_integration_id"
   | "";
   | "";
 
 
 export type InfraCredentials = {
 export type InfraCredentials = {

+ 4 - 3
ee/api/types/cred_exchange.go

@@ -11,7 +11,8 @@ type CredentialsExchangeRequest struct {
 }
 }
 
 
 type CredentialsExchangeResponse struct {
 type CredentialsExchangeResponse struct {
-	DO  *credentials.OAuthCredential `json:"do,omitempty"`
-	GCP *credentials.GCPCredential   `json:"gcp,omitempty"`
-	AWS *credentials.AWSCredential   `json:"aws,omitempty"`
+	DO    *credentials.OAuthCredential `json:"do,omitempty"`
+	GCP   *credentials.GCPCredential   `json:"gcp,omitempty"`
+	AWS   *credentials.AWSCredential   `json:"aws,omitempty"`
+	Azure *credentials.AzureCredential `json:"azure,omitempty"`
 }
 }

+ 11 - 0
internal/kubernetes/config.go

@@ -367,6 +367,17 @@ func (conf *OutOfClusterConfig) CreateRawConfigFromCluster() (*api.Config, error
 
 
 		// add this as a bearer token
 		// add this as a bearer token
 		authInfoMap[authInfoName].Token = tok
 		authInfoMap[authInfoName].Token = tok
+	case models.Azure:
+		azInt, err := conf.Repo.AzureIntegration().ReadAzureIntegration(
+			cluster.ProjectID,
+			cluster.AzureIntegrationID,
+		)
+
+		if err != nil {
+			return nil, err
+		}
+
+		authInfoMap[authInfoName].Token = string(azInt.AKSPassword)
 	default:
 	default:
 		return nil, errors.New("not a supported auth mechanism")
 		return nil, errors.New("not a supported auth mechanism")
 	}
 	}

+ 9 - 5
internal/models/cluster.go

@@ -20,6 +20,7 @@ const (
 	GCP       ClusterAuth = "gcp-sa"
 	GCP       ClusterAuth = "gcp-sa"
 	AWS       ClusterAuth = "aws-sa"
 	AWS       ClusterAuth = "aws-sa"
 	DO        ClusterAuth = "do-oauth"
 	DO        ClusterAuth = "do-oauth"
+	Azure     ClusterAuth = "azure-sp"
 	Local     ClusterAuth = "local"
 	Local     ClusterAuth = "local"
 	InCluster ClusterAuth = "in-cluster"
 	InCluster ClusterAuth = "in-cluster"
 )
 )
@@ -59,11 +60,12 @@ type Cluster struct {
 	// ------------------------------------------------------------------
 	// ------------------------------------------------------------------
 
 
 	// The various auth mechanisms available to the integration
 	// The various auth mechanisms available to the integration
-	KubeIntegrationID uint
-	OIDCIntegrationID uint
-	GCPIntegrationID  uint
-	AWSIntegrationID  uint
-	DOIntegrationID   uint
+	KubeIntegrationID  uint
+	OIDCIntegrationID  uint
+	GCPIntegrationID   uint
+	AWSIntegrationID   uint
+	DOIntegrationID    uint
+	AzureIntegrationID uint
 
 
 	// A token cache that can be used by an auth mechanism, if desired
 	// A token cache that can be used by an auth mechanism, if desired
 	TokenCache   integrations.ClusterTokenCache `json:"token_cache" gorm:"-" sql:"-"`
 	TokenCache   integrations.ClusterTokenCache `json:"token_cache" gorm:"-" sql:"-"`
@@ -83,6 +85,8 @@ func (c *Cluster) ToClusterType() *types.Cluster {
 		serv = types.GKE
 		serv = types.GKE
 	} else if c.DOIntegrationID != 0 {
 	} else if c.DOIntegrationID != 0 {
 		serv = types.DOKS
 		serv = types.DOKS
+	} else if c.AzureIntegrationID != 0 {
+		serv = types.AKS
 	}
 	}
 
 
 	return &types.Cluster{
 	return &types.Cluster{

+ 4 - 3
internal/models/cred_exchange_token.go

@@ -13,9 +13,10 @@ type CredentialsExchangeToken struct {
 	Token     []byte
 	Token     []byte
 	Expiry    *time.Time
 	Expiry    *time.Time
 
 
-	DOCredentialID  uint
-	AWSCredentialID uint
-	GCPCredentialID uint
+	DOCredentialID    uint
+	AWSCredentialID   uint
+	GCPCredentialID   uint
+	AzureCredentialID uint
 }
 }
 
 
 func (t *CredentialsExchangeToken) IsExpired() bool {
 func (t *CredentialsExchangeToken) IsExpired() bool {

+ 3 - 0
internal/models/infra.go

@@ -51,6 +51,9 @@ type Infra struct {
 	// The AWS integration that was used to create the infra
 	// The AWS integration that was used to create the infra
 	AWSIntegrationID uint
 	AWSIntegrationID uint
 
 
+	// The Azure integration that was used to create the infra
+	AzureIntegrationID uint
+
 	// The GCP integration that was used to create the infra
 	// The GCP integration that was used to create the infra
 	GCPIntegrationID uint
 	GCPIntegrationID uint
 
 

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

@@ -41,6 +41,9 @@ type AzureIntegration struct {
 	// The ACR passwords, if set
 	// The ACR passwords, if set
 	ACRPassword1 []byte `json:"acr_password_1"`
 	ACRPassword1 []byte `json:"acr_password_1"`
 	ACRPassword2 []byte `json:"acr_password_2"`
 	ACRPassword2 []byte `json:"acr_password_2"`
+
+	// The AKS password, if set (used for bearer token auth)
+	AKSPassword []byte `json:"aks_password"`
 }
 }
 
 
 func (a *AzureIntegration) ToAzureIntegrationType() *types.AzureIntegration {
 func (a *AzureIntegration) ToAzureIntegrationType() *types.AzureIntegration {

+ 8 - 2
internal/repository/credentials/credentials.go

@@ -40,12 +40,18 @@ type AWSCredential struct {
 }
 }
 
 
 type AzureCredential struct {
 type AzureCredential struct {
+	SubscriptionID string `json:"subscription_id"`
+	TenantID       string `json:"tenant_id"`
+	ClientID       string `json:"client_id"`
+
 	// The Azure service principal key
 	// The Azure service principal key
 	ServicePrincipalSecret []byte `json:"service_principal_secret"`
 	ServicePrincipalSecret []byte `json:"service_principal_secret"`
 
 
 	// The ACR passwords, if set
 	// The ACR passwords, if set
-	ACRPassword1 []byte `json:"acr_password_1"`
-	ACRPassword2 []byte `json:"acr_password_2"`
+	ACRPassword1 []byte `json:"acr_password_1,omitempty"`
+	ACRPassword2 []byte `json:"acr_password_2,omitempty"`
+
+	AKSPassword []byte `json:"aks_password,omitempty"`
 }
 }
 
 
 type CredentialStorage interface {
 type CredentialStorage interface {

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

@@ -1352,9 +1352,11 @@ func (repo *AzureIntegrationRepository) CreateAzureIntegration(
 		credentialData.ServicePrincipalSecret = az.ServicePrincipalSecret
 		credentialData.ServicePrincipalSecret = az.ServicePrincipalSecret
 		credentialData.ACRPassword1 = az.ACRPassword1
 		credentialData.ACRPassword1 = az.ACRPassword1
 		credentialData.ACRPassword2 = az.ACRPassword2
 		credentialData.ACRPassword2 = az.ACRPassword2
+		credentialData.AKSPassword = az.AKSPassword
 		az.ServicePrincipalSecret = []byte{}
 		az.ServicePrincipalSecret = []byte{}
 		az.ACRPassword1 = []byte{}
 		az.ACRPassword1 = []byte{}
 		az.ACRPassword2 = []byte{}
 		az.ACRPassword2 = []byte{}
+		az.AKSPassword = []byte{}
 	}
 	}
 
 
 	project := &models.Project{}
 	project := &models.Project{}
@@ -1402,9 +1404,11 @@ func (repo *AzureIntegrationRepository) OverwriteAzureIntegration(
 		credentialData.ServicePrincipalSecret = az.ServicePrincipalSecret
 		credentialData.ServicePrincipalSecret = az.ServicePrincipalSecret
 		credentialData.ACRPassword1 = az.ACRPassword1
 		credentialData.ACRPassword1 = az.ACRPassword1
 		credentialData.ACRPassword2 = az.ACRPassword2
 		credentialData.ACRPassword2 = az.ACRPassword2
+		credentialData.AKSPassword = az.AKSPassword
 		az.ServicePrincipalSecret = []byte{}
 		az.ServicePrincipalSecret = []byte{}
 		az.ACRPassword1 = []byte{}
 		az.ACRPassword1 = []byte{}
 		az.ACRPassword2 = []byte{}
 		az.ACRPassword2 = []byte{}
+		az.AKSPassword = []byte{}
 	}
 	}
 
 
 	if err := repo.db.Save(az).Error; err != nil {
 	if err := repo.db.Save(az).Error; err != nil {
@@ -1443,6 +1447,7 @@ func (repo *AzureIntegrationRepository) ReadAzureIntegration(
 		az.ServicePrincipalSecret = credentialData.ServicePrincipalSecret
 		az.ServicePrincipalSecret = credentialData.ServicePrincipalSecret
 		az.ACRPassword1 = credentialData.ACRPassword1
 		az.ACRPassword1 = credentialData.ACRPassword1
 		az.ACRPassword2 = credentialData.ACRPassword2
 		az.ACRPassword2 = credentialData.ACRPassword2
+		az.AKSPassword = credentialData.AKSPassword
 	}
 	}
 
 
 	err := repo.DecryptAzureIntegrationData(az, repo.key)
 	err := repo.DecryptAzureIntegrationData(az, repo.key)
@@ -1504,6 +1509,16 @@ func (repo *AzureIntegrationRepository) EncryptAzureIntegrationData(
 		az.ACRPassword2 = cipherData
 		az.ACRPassword2 = cipherData
 	}
 	}
 
 
+	if len(az.AKSPassword) > 0 {
+		cipherData, err := encryption.Encrypt(az.AKSPassword, key)
+
+		if err != nil {
+			return err
+		}
+
+		az.AKSPassword = cipherData
+	}
+
 	return nil
 	return nil
 }
 }
 
 
@@ -1543,5 +1558,15 @@ func (repo *AzureIntegrationRepository) DecryptAzureIntegrationData(
 		az.ACRPassword2 = plaintext
 		az.ACRPassword2 = plaintext
 	}
 	}
 
 
+	if len(az.AKSPassword) > 0 {
+		plaintext, err := encryption.Decrypt(az.AKSPassword, key)
+
+		if err != nil {
+			return err
+		}
+
+		az.AKSPassword = plaintext
+	}
+
 	return nil
 	return nil
 }
 }

+ 14 - 0
provisioner/server/handlers/credentials/get_credentials_ee.go

@@ -100,6 +100,20 @@ func (c *CredentialsGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 			AWSSessionToken:    awsInt.AWSSessionToken,
 			AWSSessionToken:    awsInt.AWSSessionToken,
 			AWSRegion:          []byte(awsInt.AWSRegion),
 			AWSRegion:          []byte(awsInt.AWSRegion),
 		}
 		}
+	} else if ceToken.AzureCredentialID != 0 {
+		azInt, err := repo.AzureIntegration().ReadAzureIntegration(ceToken.ProjectID, ceToken.AzureCredentialID)
+
+		if err != nil {
+			apierrors.HandleAPIError(c.config.Logger, c.config.Alerter, w, r, apierrors.NewErrForbidden(err), true)
+			return
+		}
+
+		resp.Azure = &credentials.AzureCredential{
+			SubscriptionID:         azInt.AzureSubscriptionID,
+			TenantID:               azInt.AzureTenantID,
+			ClientID:               azInt.AzureClientID,
+			ServicePrincipalSecret: azInt.ServicePrincipalSecret,
+		}
 	}
 	}
 
 
 	// return the decrypted credentials
 	// return the decrypted credentials

+ 9 - 8
provisioner/server/handlers/provision/apply.go

@@ -145,7 +145,7 @@ func (c *ProvisionApplyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 
 	// if this is a cluster or registry infra type, send to analytics client
 	// if this is a cluster or registry infra type, send to analytics client
 	switch infra.Kind {
 	switch infra.Kind {
-	case types.InfraDOKS, types.InfraEKS, types.InfraGKE:
+	case types.InfraDOKS, types.InfraEKS, types.InfraGKE, types.InfraAKS:
 		c.Config.AnalyticsClient.Track(analytics.ClusterProvisioningStartTrack(
 		c.Config.AnalyticsClient.Track(analytics.ClusterProvisioningStartTrack(
 			&analytics.ClusterProvisioningStartTrackOpts{
 			&analytics.ClusterProvisioningStartTrackOpts{
 				ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(0, infra.ProjectID),
 				ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(0, infra.ProjectID),
@@ -153,7 +153,7 @@ func (c *ProvisionApplyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 				InfraID:                infra.ID,
 				InfraID:                infra.ID,
 			},
 			},
 		))
 		))
-	case types.InfraDOCR, types.InfraECR, types.InfraGCR:
+	case types.InfraDOCR, types.InfraECR, types.InfraGCR, types.InfraACR:
 		c.Config.AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
 		c.Config.AnalyticsClient.Track(analytics.RegistryProvisioningStartTrack(
 			&analytics.RegistryProvisioningStartTrackOpts{
 			&analytics.RegistryProvisioningStartTrackOpts{
 				ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(0, infra.ProjectID),
 				ProjectScopedTrackOpts: analytics.GetProjectScopedTrackOpts(0, infra.ProjectID),
@@ -181,12 +181,13 @@ func createCredentialsExchangeToken(conf *config.Config, infra *models.Infra) (*
 	}
 	}
 
 
 	ceToken := &models.CredentialsExchangeToken{
 	ceToken := &models.CredentialsExchangeToken{
-		ProjectID:       infra.ProjectID,
-		Expiry:          &expiry,
-		Token:           hashedToken,
-		DOCredentialID:  infra.DOIntegrationID,
-		AWSCredentialID: infra.AWSIntegrationID,
-		GCPCredentialID: infra.GCPIntegrationID,
+		ProjectID:         infra.ProjectID,
+		Expiry:            &expiry,
+		Token:             hashedToken,
+		DOCredentialID:    infra.DOIntegrationID,
+		AWSCredentialID:   infra.AWSIntegrationID,
+		GCPCredentialID:   infra.GCPIntegrationID,
+		AzureCredentialID: infra.AzureIntegrationID,
 	}
 	}
 
 
 	// handle write to the database
 	// handle write to the database

+ 35 - 1
provisioner/server/handlers/state/create_resource.go

@@ -86,7 +86,7 @@ func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 
 	// switch on the kind of resource and write the corresponding objects to the database
 	// switch on the kind of resource and write the corresponding objects to the database
 	switch req.Kind {
 	switch req.Kind {
-	case string(types.InfraEKS), string(types.InfraDOKS), string(types.InfraGKE):
+	case string(types.InfraEKS), string(types.InfraDOKS), string(types.InfraGKE), string(types.InfraAKS):
 		var cluster *models.Cluster
 		var cluster *models.Cluster
 
 
 		cluster, err = createCluster(c.Config, infra, operation, req.Output)
 		cluster, err = createCluster(c.Config, infra, operation, req.Output)
@@ -108,6 +108,8 @@ func (c *CreateResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		_, err = createDOCRRegistry(c.Config, infra, operation, req.Output)
 		_, err = createDOCRRegistry(c.Config, infra, operation, req.Output)
 	case string(types.InfraGCR):
 	case string(types.InfraGCR):
 		_, err = createGCRRegistry(c.Config, infra, operation, req.Output)
 		_, err = createGCRRegistry(c.Config, infra, operation, req.Output)
+	case string(types.InfraACR):
+		_, err = createACRRegistry(c.Config, infra, operation, req.Output)
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
@@ -244,6 +246,23 @@ func createCluster(config *config.Config, infra *models.Infra, operation *models
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	// if cluster_token is output and infra is azure, update the azure integration
+	if _, exists := output["cluster_token"]; exists && infra.AzureIntegrationID != 0 {
+		azInt, err := config.Repo.AzureIntegration().ReadAzureIntegration(infra.ProjectID, infra.AzureIntegrationID)
+
+		if err != nil {
+			return nil, err
+		}
+
+		azInt.AKSPassword = []byte(output["cluster_token"].(string))
+
+		azInt, err = config.Repo.AzureIntegration().OverwriteAzureIntegration(azInt)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	cluster.Name = output["cluster_name"].(string)
 	cluster.Name = output["cluster_name"].(string)
 	cluster.Server = output["cluster_endpoint"].(string)
 	cluster.Server = output["cluster_endpoint"].(string)
 	cluster.CertificateAuthorityData = caData
 	cluster.CertificateAuthorityData = caData
@@ -277,6 +296,9 @@ func getNewCluster(infra *models.Infra) *models.Cluster {
 	case types.InfraDOKS:
 	case types.InfraDOKS:
 		res.AuthMechanism = models.DO
 		res.AuthMechanism = models.DO
 		res.DOIntegrationID = infra.DOIntegrationID
 		res.DOIntegrationID = infra.DOIntegrationID
+	case types.InfraAKS:
+		res.AuthMechanism = models.Azure
+		res.AzureIntegrationID = infra.AzureIntegrationID
 	}
 	}
 
 
 	return res
 	return res
@@ -326,6 +348,18 @@ func createGCRRegistry(config *config.Config, infra *models.Infra, operation *mo
 	return config.Repo.Registry().CreateRegistry(reg)
 	return config.Repo.Registry().CreateRegistry(reg)
 }
 }
 
 
+func createACRRegistry(config *config.Config, infra *models.Infra, operation *models.Operation, output map[string]interface{}) (*models.Registry, error) {
+	reg := &models.Registry{
+		ProjectID:          infra.ProjectID,
+		AzureIntegrationID: infra.AzureIntegrationID,
+		InfraID:            infra.ID,
+		URL:                output["url"].(string),
+		Name:               output["name"].(string),
+	}
+
+	return config.Repo.Registry().CreateRegistry(reg)
+}
+
 func createRDSEnvGroup(config *config.Config, infra *models.Infra, database *models.Database, lastApplied map[string]interface{}) error {
 func createRDSEnvGroup(config *config.Config, infra *models.Infra, database *models.Database, lastApplied map[string]interface{}) error {
 	cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)
 	cluster, err := config.Repo.Cluster().ReadCluster(infra.ProjectID, infra.ParentClusterID)