Просмотр исходного кода

Merge pull request #2046 from porter-dev/belanger/azure-provisioner

Add Azure provisioning support and integration (AKS and ACR)
abelanger5 4 лет назад
Родитель
Сommit
ade17b68c8
32 измененных файлов с 752 добавлено и 83 удалено
  1. 19 1
      api/server/handlers/infra/create.go
  2. 75 0
      api/server/handlers/infra/forms.go
  3. 4 0
      api/server/handlers/infra/get_template.go
  4. 16 0
      api/server/handlers/infra/list_templates.go
  5. 44 0
      api/server/handlers/project_integration/list_azure.go
  6. 27 0
      api/server/router/project_integration.go
  7. 1 0
      api/types/cluster.go
  8. 9 3
      api/types/infra.go
  9. 2 0
      api/types/project_integration.go
  10. 12 7
      dashboard/src/components/ProvisionerStatus.tsx
  11. 10 8
      dashboard/src/main/home/infrastructure/ExpandedInfra.tsx
  12. 4 1
      dashboard/src/main/home/infrastructure/components/DeployList.tsx
  13. 45 15
      dashboard/src/main/home/infrastructure/components/ExpandedOperation.tsx
  14. 41 23
      dashboard/src/main/home/infrastructure/components/InfraSettings.tsx
  15. 17 0
      dashboard/src/main/home/infrastructure/components/ProvisionInfra.tsx
  16. 140 0
      dashboard/src/main/home/infrastructure/components/credentials/AzureCredentialForm.tsx
  17. 110 0
      dashboard/src/main/home/infrastructure/components/credentials/AzureCredentialList.tsx
  18. 20 0
      dashboard/src/shared/api.tsx
  19. 12 1
      dashboard/src/shared/common.tsx
  20. 17 0
      dashboard/src/shared/types.tsx
  21. 4 3
      ee/api/types/cred_exchange.go
  22. 11 0
      internal/kubernetes/config.go
  23. 9 5
      internal/models/cluster.go
  24. 4 3
      internal/models/cred_exchange_token.go
  25. 3 0
      internal/models/infra.go
  26. 3 0
      internal/models/integrations/azure.go
  27. 8 2
      internal/repository/credentials/credentials.go
  28. 25 0
      internal/repository/gorm/auth.go
  29. 14 0
      provisioner/server/handlers/credentials/get_credentials_ee.go
  30. 9 8
      provisioner/server/handlers/provision/apply.go
  31. 35 1
      provisioner/server/handlers/state/create_resource.go
  32. 2 2
      provisioner/server/handlers/state/delete_resource.go

+ 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

+ 12 - 7
dashboard/src/components/ProvisionerStatus.tsx

@@ -394,16 +394,21 @@ type OperationDetailsProps = {
   infra: Infrastructure;
   infra: Infrastructure;
   can_delete?: boolean;
   can_delete?: boolean;
   refreshInfra: (completed?: boolean, errored?: boolean) => void;
   refreshInfra: (completed?: boolean, errored?: boolean) => void;
+  useOperation?: Operation;
+  padding?: string;
 };
 };
 
 
-const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
+export const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
   infra,
   infra,
   can_delete,
   can_delete,
   refreshInfra,
   refreshInfra,
+  useOperation,
+  padding,
 }) => {
 }) => {
-  const [isLoading, setIsLoading] = useState(true);
+  const [isLoading, setIsLoading] = useState(!useOperation);
   const [hasError, setHasError] = useState(false);
   const [hasError, setHasError] = useState(false);
-  const [operation, setOperation] = useState<Operation>(null);
+  const [operation, setOperation] = useState<Operation>(useOperation);
+
   const [infraState, setInfraState] = useState<TFState>(null);
   const [infraState, setInfraState] = useState<TFState>(null);
   const [infraStateInitialized, setInfraStateInitialized] = useState(false);
   const [infraStateInitialized, setInfraStateInitialized] = useState(false);
   const { currentProject, setCurrentError } = useContext(Context);
   const { currentProject, setCurrentError } = useContext(Context);
@@ -526,7 +531,7 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
         {
         {
           project_id: currentProject.id,
           project_id: currentProject.id,
           infra_id: infra.id,
           infra_id: infra.id,
-          operation_id: infra.latest_operation.id,
+          operation_id: useOperation?.id || infra.latest_operation.id,
         }
         }
       )
       )
       .then(({ data }) => {
       .then(({ data }) => {
@@ -760,7 +765,7 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
   };
   };
 
 
   return (
   return (
-    <StyledCard>
+    <StyledCard padding={padding}>
       {renderLoadingBar(
       {renderLoadingBar(
         createdResources.length + deletedResources.length,
         createdResources.length + deletedResources.length,
         createdResources.length +
         createdResources.length +
@@ -780,8 +785,8 @@ const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
   );
   );
 };
 };
 
 
-const StyledCard = styled.div`
-  padding: 12px 20px;
+const StyledCard = styled.div<{ padding?: string }>`
+  padding: ${(props) => props.padding || "12px 20px"};
   max-height: 300px;
   max-height: 300px;
   overflow-y: auto;
   overflow-y: auto;
 `;
 `;

+ 10 - 8
dashboard/src/main/home/infrastructure/ExpandedInfra.tsx

@@ -37,8 +37,10 @@ const ExpandedInfra: React.FunctionComponent = () => {
       return;
       return;
     }
     }
 
 
-    let isSubscribed = true;
+    refreshInfra();
+  }, [currentProject, infra_id]);
 
 
+  const refreshInfra = () => {
     api
     api
       .getInfraByID(
       .getInfraByID(
         "<token>",
         "<token>",
@@ -49,10 +51,6 @@ const ExpandedInfra: React.FunctionComponent = () => {
         }
         }
       )
       )
       .then(({ data }) => {
       .then(({ data }) => {
-        if (!isSubscribed) {
-          return;
-        }
-
         setInfra(data);
         setInfra(data);
       })
       })
       .catch((err) => {
       .catch((err) => {
@@ -60,7 +58,7 @@ const ExpandedInfra: React.FunctionComponent = () => {
         setHasError(true);
         setHasError(true);
         setCurrentError(err.response?.data?.error);
         setCurrentError(err.response?.data?.error);
       });
       });
-  }, [currentProject, infra_id]);
+  };
 
 
   useEffect(() => {
   useEffect(() => {
     if (!currentProject || !infra) {
     if (!currentProject || !infra) {
@@ -132,12 +130,16 @@ const ExpandedInfra: React.FunctionComponent = () => {
     switch (newTab) {
     switch (newTab) {
       case "deploys":
       case "deploys":
         return (
         return (
-          <DeployList infra={infra} setLatestOperation={setLatestOperation} />
+          <DeployList
+            infra={infra}
+            setLatestOperation={setLatestOperation}
+            refreshInfra={refreshInfra}
+          />
         );
         );
       case "resources":
       case "resources":
         return <InfraResourceList infra_id={infra_id} />;
         return <InfraResourceList infra_id={infra_id} />;
       case "settings":
       case "settings":
-        return <InfraSettings infra_id={infra_id} onDelete={() => {}} />;
+        return <InfraSettings infra_id={infra_id} onDelete={refreshInfra} />;
     }
     }
   };
   };
 
 

+ 4 - 1
dashboard/src/main/home/infrastructure/components/DeployList.tsx

@@ -17,11 +17,13 @@ import ExpandedOperation from "./ExpandedOperation";
 type Props = {
 type Props = {
   infra: Infrastructure;
   infra: Infrastructure;
   setLatestOperation: (operation: Operation) => void;
   setLatestOperation: (operation: Operation) => void;
+  refreshInfra: () => void;
 };
 };
 
 
 const DeployList: React.FunctionComponent<Props> = ({
 const DeployList: React.FunctionComponent<Props> = ({
   infra,
   infra,
   setLatestOperation,
   setLatestOperation,
+  refreshInfra,
 }) => {
 }) => {
   const [isLoading, setIsLoading] = useState(true);
   const [isLoading, setIsLoading] = useState(true);
   const [hasError, setHasError] = useState(false);
   const [hasError, setHasError] = useState(false);
@@ -180,8 +182,9 @@ const DeployList: React.FunctionComponent<Props> = ({
       return (
       return (
         <ExpandedOperation
         <ExpandedOperation
           operation_id={selectedOperation.id}
           operation_id={selectedOperation.id}
-          infra_id={selectedOperation.infra_id}
+          infra={infra}
           back={backFromExpandedOperation}
           back={backFromExpandedOperation}
+          refreshInfra={refreshInfra}
         />
         />
       );
       );
     }
     }

+ 45 - 15
dashboard/src/main/home/infrastructure/components/ExpandedOperation.tsx

@@ -3,7 +3,12 @@ import { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
 import styled from "styled-components";
 import styled from "styled-components";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
-import { Operation, OperationStatus, OperationType } from "shared/types";
+import {
+  Infrastructure,
+  Operation,
+  OperationStatus,
+  OperationType,
+} from "shared/types";
 import { readableDate } from "shared/string_utils";
 import { readableDate } from "shared/string_utils";
 import Placeholder from "components/Placeholder";
 import Placeholder from "components/Placeholder";
 import { useWebsockets } from "shared/hooks/useWebsockets";
 import { useWebsockets } from "shared/hooks/useWebsockets";
@@ -11,17 +16,20 @@ import Heading from "components/form-components/Heading";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import Description from "components/Description";
 import Description from "components/Description";
+import { OperationDetails } from "components/ProvisionerStatus";
 
 
 type Props = {
 type Props = {
-  infra_id: number;
+  infra: Infrastructure;
   operation_id: string;
   operation_id: string;
   back: (operation?: Operation) => void;
   back: (operation?: Operation) => void;
+  refreshInfra: () => void;
 };
 };
 
 
 const ExpandedOperation: React.FunctionComponent<Props> = ({
 const ExpandedOperation: React.FunctionComponent<Props> = ({
-  infra_id,
+  infra,
   operation_id,
   operation_id,
   back,
   back,
+  refreshInfra,
 }) => {
 }) => {
   const [isLoading, setIsLoading] = useState(true);
   const [isLoading, setIsLoading] = useState(true);
   const [hasError, setHasError] = useState(false);
   const [hasError, setHasError] = useState(false);
@@ -38,7 +46,7 @@ const ExpandedOperation: React.FunctionComponent<Props> = ({
         {},
         {},
         {
         {
           project_id: currentProject.id,
           project_id: currentProject.id,
-          infra_id: infra_id,
+          infra_id: infra.id,
           operation_id: operation_id,
           operation_id: operation_id,
         }
         }
       )
       )
@@ -109,7 +117,7 @@ const ExpandedOperation: React.FunctionComponent<Props> = ({
           {},
           {},
           {
           {
             project_id: currentProject.id,
             project_id: currentProject.id,
-            infra_id: infra_id,
+            infra_id: infra.id,
             operation_id: operation_id,
             operation_id: operation_id,
           }
           }
         )
         )
@@ -129,7 +137,7 @@ const ExpandedOperation: React.FunctionComponent<Props> = ({
   const retry = () => {
   const retry = () => {
     let pathParams = {
     let pathParams = {
       project_id: currentProject.id,
       project_id: currentProject.id,
-      infra_id: infra_id,
+      infra_id: infra.id,
     };
     };
 
 
     let apiCall = api.updateInfra;
     let apiCall = api.updateInfra;
@@ -240,6 +248,35 @@ const ExpandedOperation: React.FunctionComponent<Props> = ({
     return logs.map((l, i) => <Log key={i}>{l}</Log>);
     return logs.map((l, i) => <Log key={i}>{l}</Log>);
   };
   };
 
 
+  const renderOperationDetails = () => {
+    if (infra.latest_operation.id == operation.id) {
+      return (
+        <>
+          <Description>Infrastructure progress:</Description>
+          <OperationDetails
+            infra={infra}
+            refreshInfra={refreshInfra}
+            useOperation={operation}
+            padding={"12px 0"}
+          />
+        </>
+      );
+    }
+
+    return (
+      <>
+        <Description>
+          {getOperationDescription(
+            operation.type,
+            operation.status,
+            operation.last_updated
+          )}
+        </Description>
+        <Br />
+      </>
+    );
+  };
+
   return (
   return (
     <StyledCard>
     <StyledCard>
       <BackArrowContainer>
       <BackArrowContainer>
@@ -250,14 +287,7 @@ const ExpandedOperation: React.FunctionComponent<Props> = ({
       </BackArrowContainer>
       </BackArrowContainer>
       <MetadataContainer>
       <MetadataContainer>
         <Heading>Deployment Summary</Heading>
         <Heading>Deployment Summary</Heading>
-        <Description>
-          {getOperationDescription(
-            operation.type,
-            operation.status,
-            operation.last_updated
-          )}
-        </Description>
-        <Br />
+        {renderOperationDetails()}
         {renderRerunButton()}
         {renderRerunButton()}
       </MetadataContainer>
       </MetadataContainer>
       <MetadataContainer>
       <MetadataContainer>
@@ -332,7 +362,7 @@ const MetadataContainer = styled.div`
   margin-bottom: 3px;
   margin-bottom: 3px;
   border-radius: 6px;
   border-radius: 6px;
   background: #2e3135;
   background: #2e3135;
-  padding: 0 20px;
+  padding: 0 20px 16px 20px;
   overflow-y: auto;
   overflow-y: auto;
   min-height: 180px;
   min-height: 180px;
   font-size: 13px;
   font-size: 13px;

+ 41 - 23
dashboard/src/main/home/infrastructure/components/InfraSettings.tsx

@@ -1,18 +1,24 @@
-import React, { useContext } from "react";
+import React, { useContext, useState } from "react";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
 import styled from "styled-components";
 import styled from "styled-components";
 import Heading from "components/form-components/Heading";
 import Heading from "components/form-components/Heading";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import Description from "components/Description";
 import Description from "components/Description";
+import ConfirmOverlay from "components/ConfirmOverlay";
 
 
 type Props = {
 type Props = {
   infra_id: number;
   infra_id: number;
   onDelete: () => void;
   onDelete: () => void;
 };
 };
 
 
-const InfraSettings: React.FunctionComponent<Props> = ({ infra_id }) => {
-  const { currentProject, setCurrentError } = useContext(Context);
+const InfraSettings: React.FunctionComponent<Props> = ({
+  infra_id,
+  onDelete,
+}) => {
+  const { currentProject, setCurrentError, setCurrentOverlay } = useContext(
+    Context
+  );
 
 
   const deleteInfra = () => {
   const deleteInfra = () => {
     api
     api
@@ -24,7 +30,10 @@ const InfraSettings: React.FunctionComponent<Props> = ({ infra_id }) => {
           infra_id: infra_id,
           infra_id: infra_id,
         }
         }
       )
       )
-      .then()
+      .then(() => {
+        setCurrentOverlay(null);
+        onDelete();
+      })
       .catch((err) => {
       .catch((err) => {
         console.error(err);
         console.error(err);
         setCurrentError(err.response?.data?.error);
         setCurrentError(err.response?.data?.error);
@@ -32,25 +41,34 @@ const InfraSettings: React.FunctionComponent<Props> = ({ infra_id }) => {
   };
   };
 
 
   return (
   return (
-    <StyledCard>
-      <MetadataContainer>
-        <Heading>Delete Infrastructure</Heading>
-        <Description>
-          This will destroy all of the existing cloud infrastructure attached to
-          this module.
-        </Description>
-        <Br />
-        <SaveButton
-          onClick={deleteInfra}
-          text="Delete Infrastructure"
-          color="#b91133"
-          disabled={false}
-          makeFlush={true}
-          clearPosition={true}
-          saveText="Deletion process started, see the Deploys tab for info."
-        />
-      </MetadataContainer>
-    </StyledCard>
+    <>
+      <StyledCard>
+        <MetadataContainer>
+          <Heading>Delete Infrastructure</Heading>
+          <Description>
+            This will destroy all of the existing cloud infrastructure attached
+            to this module.
+          </Description>
+          <Br />
+
+          <SaveButton
+            onClick={() =>
+              setCurrentOverlay({
+                message: `Are you sure you want to delete this infrastructure?`,
+                onYes: deleteInfra,
+                onNo: () => setCurrentOverlay(null),
+              })
+            }
+            text="Delete Infrastructure"
+            color="#b91133"
+            disabled={false}
+            makeFlush={true}
+            clearPosition={true}
+            saveText="Deletion process started, see the Deploys tab for info."
+          />
+        </MetadataContainer>
+      </StyledCard>
+    </>
   );
   );
 };
 };
 
 

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

+ 140 - 0
dashboard/src/main/home/infrastructure/components/credentials/AzureCredentialForm.tsx

@@ -0,0 +1,140 @@
+import React, { useContext, useState } from "react";
+import InputRow from "components/form-components/InputRow";
+import SaveButton from "components/SaveButton";
+
+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";
+
+type Props = {
+  setCreatedCredential: (aws_integration_id: number) => void;
+  cancel: () => void;
+};
+
+const AzureCredentialForm: React.FunctionComponent<Props> = ({
+  setCreatedCredential,
+}) => {
+  const { currentProject, setCurrentError } = useContext(Context);
+  const [clientId, setClientId] = useState("");
+  const [servicePrincipalKey, setServicePrincipalKey] = useState("");
+  const [tenantId, setTenantId] = useState("");
+  const [subscriptionId, setSubscriptionId] = useState("");
+  const [buttonStatus, setButtonStatus] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const [hasError, setHasError] = useState(false);
+
+  const submit = () => {
+    setIsLoading(true);
+
+    api
+      .createAzureIntegration(
+        "<token>",
+        {
+          azure_client_id: clientId,
+          azure_subscription_id: subscriptionId,
+          azure_tenant_id: tenantId,
+          service_principal_key: servicePrincipalKey,
+        },
+        {
+          id: currentProject.id,
+        }
+      )
+      .then(({ data }) => {
+        setCreatedCredential(data.id);
+        setIsLoading(false);
+      })
+      .catch((err) => {
+        console.error(err);
+        setHasError(true);
+        setCurrentError(err.response?.data?.error);
+        setIsLoading(false);
+      });
+  };
+
+  if (hasError) {
+    return <Placeholder>Error</Placeholder>;
+  }
+
+  if (isLoading) {
+    return (
+      <Placeholder>
+        <Loading />
+      </Placeholder>
+    );
+  }
+
+  return (
+    <>
+      <InputRow
+        type="text"
+        value={clientId}
+        setValue={(x: string) => {
+          setClientId(x);
+        }}
+        label="👤 Azure Client ID"
+        placeholder="ex. 12345678-abcd-1234-abcd-12345678abcd"
+        width="100%"
+        isRequired={true}
+      />
+      <InputRow
+        type="password"
+        value={servicePrincipalKey}
+        setValue={(x: string) => {
+          setServicePrincipalKey(x);
+        }}
+        label="🔒 Azure Service Principal Key"
+        placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+        width="100%"
+        isRequired={true}
+      />
+      <InputRow
+        type="text"
+        value={tenantId}
+        setValue={(x: string) => {
+          setTenantId(x);
+        }}
+        label="Azure Tenant ID"
+        placeholder="ex. 12345678-abcd-1234-abcd-12345678abcd"
+        width="100%"
+        isRequired={true}
+      />
+      <InputRow
+        type="text"
+        value={subscriptionId}
+        setValue={(x: string) => {
+          setSubscriptionId(x);
+        }}
+        label="Azure Subscription ID"
+        placeholder="ex. 12345678-abcd-1234-abcd-12345678abcd"
+        width="100%"
+        isRequired={true}
+      />
+      <Flex>
+        <SaveButton
+          text="Continue"
+          disabled={false}
+          onClick={submit}
+          makeFlush={true}
+          clearPosition={true}
+          status={buttonStatus}
+          statusPosition={"right"}
+        />
+      </Flex>
+    </>
+  );
+};
+
+export default AzureCredentialForm;
+
+const Flex = styled.div`
+  display: flex;
+  color: #ffffff;
+  align-items: center;
+  > i {
+    color: #aaaabb;
+    font-size: 20px;
+    margin-right: 10px;
+  }
+`;

+ 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;
+`;

+ 20 - 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;
@@ -82,6 +87,18 @@ const overwriteAWSIntegration = baseApi<
   return `/api/projects/${pathParams.project_id}/integrations/aws/overwrite`;
   return `/api/projects/${pathParams.project_id}/integrations/aws/overwrite`;
 });
 });
 
 
+const createAzureIntegration = baseApi<
+  {
+    azure_client_id: string;
+    azure_subscription_id: string;
+    azure_tenant_id: string;
+    service_principal_key: string;
+  },
+  { id: number }
+>("POST", (pathParams) => {
+  return `/api/projects/${pathParams.id}/integrations/azure`;
+});
+
 const createEmailVerification = baseApi<{}, {}>("POST", (pathParams) => {
 const createEmailVerification = baseApi<{}, {}>("POST", (pathParams) => {
   return `/api/email/verify/initiate`;
   return `/api/email/verify/initiate`;
 });
 });
@@ -709,6 +726,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,8 +1805,10 @@ export default {
   connectDORegistry,
   connectDORegistry,
   getAWSIntegration,
   getAWSIntegration,
   getGCPIntegration,
   getGCPIntegration,
+  getAzureIntegration,
   createAWSIntegration,
   createAWSIntegration,
   overwriteAWSIntegration,
   overwriteAWSIntegration,
+  createAzureIntegration,
   createEmailVerification,
   createEmailVerification,
   createEnvironment,
   createEnvironment,
   deleteEnvironment,
   deleteEnvironment,

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

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

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