Browse Source

add azure support behind feature flag (#3153)

* add azure support behind feature flag

* remove api-contracts

---------

Co-authored-by: David Townley <davidtownley@Davids-MacBook-Air.local>
d-g-town 2 years ago
parent
commit
a140ba319a

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

@@ -3,6 +3,10 @@ package project_integration
 import (
 	"net/http"
 
+	"github.com/bufbuild/connect-go"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+
 	"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"
@@ -48,6 +52,20 @@ func (p *CreateAzureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		AzureIntegration: az.ToAzureIntegrationType(),
 	}
 
+	req := connect.NewRequest(&porterv1.CreateAzureConnectionRequest{
+		ProjectId:              int64(project.ID),
+		ClientId:               request.AzureClientID,
+		SubscriptionId:         request.AzureSubscriptionID,
+		TenantId:               request.AzureTenantID,
+		ServicePrincipalSecret: []byte(request.ServicePrincipalKey),
+	})
+	_, err = p.Config().ClusterControlPlaneClient.CreateAzureConnection(r.Context(), req)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	p.WriteResult(w, r, res)
 }
 

+ 77 - 191
dashboard/package-lock.json

@@ -12,7 +12,7 @@
         "@loadable/component": "^5.15.2",
         "@material-ui/core": "^4.11.3",
         "@material-ui/lab": "^4.0.0-alpha.61",
-        "@porter-dev/api-contracts": "^0.0.41",
+        "@porter-dev/api-contracts": "^0.0.63",
         "@sentry/react": "^6.13.2",
         "@sentry/tracing": "^6.13.2",
         "@tanstack/react-query": "^4.13.0",
@@ -1995,6 +1995,11 @@
       "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
       "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
     },
+    "node_modules/@emotion/serialize/node_modules/csstype": {
+      "version": "2.6.21",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
+      "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
+    },
     "node_modules/@emotion/sheet": {
       "version": "0.9.4",
       "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz",
@@ -2261,6 +2266,11 @@
         }
       }
     },
+    "node_modules/@material-ui/styles/node_modules/csstype": {
+      "version": "2.6.21",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
+      "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
+    },
     "node_modules/@material-ui/system": {
       "version": "4.12.2",
       "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz",
@@ -2289,6 +2299,11 @@
         }
       }
     },
+    "node_modules/@material-ui/system/node_modules/csstype": {
+      "version": "2.6.21",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
+      "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
+    },
     "node_modules/@material-ui/types": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
@@ -2415,9 +2430,9 @@
       }
     },
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.0.41",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.41.tgz",
-      "integrity": "sha512-3KzqDgHTqxjOkzdfD1U+e+RL8j7R7nLkr77xxrLyxAsZP8CXwETS5VXs76k5Sj/62dHv6E2v5wnAwy+skYWr3w==",
+      "version": "0.0.63",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.63.tgz",
+      "integrity": "sha512-ItrMS4ifqf/1BhbXj29pqgeTZzC0z6P7QaCppLF24sYFgoEvCUg/O6Ve0PxrpEjdNg3Dx30Qmy0gavcJODGV5g==",
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
       }
@@ -3081,7 +3096,7 @@
       "version": "18.0.28",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
       "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
-      "dev": true,
+      "devOptional": true,
       "dependencies": {
         "@types/prop-types": "*",
         "@types/scheduler": "*",
@@ -3185,17 +3200,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@types/react-transition-group/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
-    "node_modules/@types/react/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
-      "dev": true
-    },
     "node_modules/@types/reactcss": {
       "version": "1.2.6",
       "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz",
@@ -3243,12 +3247,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@types/styled-components/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
-      "dev": true
-    },
     "node_modules/@types/tapable": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz",
@@ -3512,11 +3510,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/axis/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/bounds": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-1.7.0.tgz",
@@ -3549,11 +3542,6 @@
         "@types/react": "*"
       }
     },
-    "node_modules/@visx/bounds/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/curve": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-1.7.0.tgz",
@@ -3582,11 +3570,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/event/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/gradient": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/@visx/gradient/-/gradient-1.7.0.tgz",
@@ -3609,11 +3592,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/gradient/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/grid": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-1.17.1.tgz",
@@ -3642,11 +3620,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/grid/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/group": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@visx/group/-/group-1.17.1.tgz",
@@ -3670,11 +3643,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/group/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/mock-data": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/@visx/mock-data/-/mock-data-1.7.0.tgz",
@@ -3714,11 +3682,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/responsive/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/scale": {
       "version": "1.14.0",
       "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-1.14.0.tgz",
@@ -3764,11 +3727,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/shape/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/text": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@visx/text/-/text-1.17.1.tgz",
@@ -3795,11 +3753,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/text/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@visx/tooltip": {
       "version": "1.17.1",
       "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-1.17.1.tgz",
@@ -3826,11 +3779,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/@visx/tooltip/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/@webassemblyjs/ast": {
       "version": "1.9.0",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@@ -5864,9 +5812,9 @@
       }
     },
     "node_modules/csstype": {
-      "version": "2.6.21",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
-      "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
     },
     "node_modules/cyclist": {
       "version": "1.0.1",
@@ -6275,11 +6223,6 @@
         "csstype": "^3.0.2"
       }
     },
-    "node_modules/dom-helpers/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/dom-serializer": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@@ -9113,11 +9056,6 @@
         "jss": "10.10.0"
       }
     },
-    "node_modules/jss/node_modules/csstype": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-      "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-    },
     "node_modules/jszip": {
       "version": "3.10.1",
       "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -16338,6 +16276,11 @@
           "version": "0.7.4",
           "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
           "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
+        },
+        "csstype": {
+          "version": "2.6.21",
+          "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
+          "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
         }
       }
     },
@@ -16375,7 +16318,8 @@
     "@icons/material": {
       "version": "0.2.4",
       "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
-      "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="
+      "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
+      "requires": {}
     },
     "@ironplans/api": {
       "version": "0.4.1",
@@ -16525,6 +16469,13 @@
         "jss-plugin-rule-value-function": "^10.5.1",
         "jss-plugin-vendor-prefixer": "^10.5.1",
         "prop-types": "^15.7.2"
+      },
+      "dependencies": {
+        "csstype": {
+          "version": "2.6.21",
+          "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
+          "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
+        }
       }
     },
     "@material-ui/system": {
@@ -16536,12 +16487,20 @@
         "@material-ui/utils": "^4.11.3",
         "csstype": "^2.5.2",
         "prop-types": "^15.7.2"
+      },
+      "dependencies": {
+        "csstype": {
+          "version": "2.6.21",
+          "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
+          "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
+        }
       }
     },
     "@material-ui/types": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
-      "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A=="
+      "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
+      "requires": {}
     },
     "@material-ui/utils": {
       "version": "4.11.3",
@@ -16607,9 +16566,9 @@
       "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
     },
     "@porter-dev/api-contracts": {
-      "version": "0.0.41",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.41.tgz",
-      "integrity": "sha512-3KzqDgHTqxjOkzdfD1U+e+RL8j7R7nLkr77xxrLyxAsZP8CXwETS5VXs76k5Sj/62dHv6E2v5wnAwy+skYWr3w==",
+      "version": "0.0.63",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.63.tgz",
+      "integrity": "sha512-ItrMS4ifqf/1BhbXj29pqgeTZzC0z6P7QaCppLF24sYFgoEvCUg/O6Ve0PxrpEjdNg3Dx30Qmy0gavcJODGV5g==",
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"
       }
@@ -16856,7 +16815,8 @@
       "version": "7.2.1",
       "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz",
       "integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "@types/body-parser": {
       "version": "1.19.2",
@@ -17169,19 +17129,11 @@
       "version": "18.0.28",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz",
       "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==",
-      "dev": true,
+      "devOptional": true,
       "requires": {
         "@types/prop-types": "*",
         "@types/scheduler": "*",
         "csstype": "^3.0.2"
-      },
-      "dependencies": {
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
-          "dev": true
-        }
       }
     },
     "@types/react-addons-linked-state-mixin": {
@@ -17280,11 +17232,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17333,14 +17280,6 @@
         "@types/hoist-non-react-statics": "*",
         "@types/react": "*",
         "csstype": "^3.0.2"
-      },
-      "dependencies": {
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
-          "dev": true
-        }
       }
     },
     "@types/tapable": {
@@ -17576,11 +17515,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17611,11 +17545,6 @@
           "requires": {
             "@types/react": "*"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17646,11 +17575,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17672,11 +17596,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17704,11 +17623,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17731,11 +17645,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17774,11 +17683,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17823,11 +17727,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17853,11 +17752,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -17882,11 +17776,6 @@
             "@types/scheduler": "*",
             "csstype": "^3.0.2"
           }
-        },
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
         }
       }
     },
@@ -18136,13 +18025,15 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
       "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "ajv-keywords": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
       "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "anser": {
       "version": "2.1.1",
@@ -19584,9 +19475,9 @@
       "dev": true
     },
     "csstype": {
-      "version": "2.6.21",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
-      "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
     },
     "cyclist": {
       "version": "1.0.1",
@@ -19929,13 +19820,6 @@
       "requires": {
         "@babel/runtime": "^7.8.7",
         "csstype": "^3.0.2"
-      },
-      "dependencies": {
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-        }
       }
     },
     "dom-serializer": {
@@ -21066,7 +20950,8 @@
     "goober": {
       "version": "2.1.12",
       "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz",
-      "integrity": "sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q=="
+      "integrity": "sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q==",
+      "requires": {}
     },
     "good-listener": {
       "version": "1.2.2",
@@ -21452,7 +21337,8 @@
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
       "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "ieee754": {
       "version": "1.2.1",
@@ -22088,13 +21974,6 @@
         "csstype": "^3.0.2",
         "is-in-browser": "^1.1.3",
         "tiny-warning": "^1.0.2"
-      },
-      "dependencies": {
-        "csstype": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
-          "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
-        }
       }
     },
     "jss-plugin-camel-case": {
@@ -22336,7 +22215,8 @@
     "markdown-to-jsx": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.2.0.tgz",
-      "integrity": "sha512-3l4/Bigjm4bEqjCR6Xr+d4DtM1X6vvtGsMGSjJYyep8RjjIvcWtrXBS8Wbfe1/P+atKNMccpsraESIaWVplzVg=="
+      "integrity": "sha512-3l4/Bigjm4bEqjCR6Xr+d4DtM1X6vvtGsMGSjJYyep8RjjIvcWtrXBS8Wbfe1/P+atKNMccpsraESIaWVplzVg==",
+      "requires": {}
     },
     "material-colors": {
       "version": "1.2.6",
@@ -23254,7 +23134,8 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
       "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "postcss-modules-local-by-default": {
       "version": "4.0.0",
@@ -23545,7 +23426,8 @@
     "react-animate-height": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/react-animate-height/-/react-animate-height-3.1.1.tgz",
-      "integrity": "sha512-UkC6+V3ZlCneBRaSM7aUctDJ+PRP6ztcGtxvU7MTeoMMWPhz8BQNaX7QWaZrkzp1ih1G8uZZ+DI9nfLvtD6OdQ=="
+      "integrity": "sha512-UkC6+V3ZlCneBRaSM7aUctDJ+PRP6ztcGtxvU7MTeoMMWPhz8BQNaX7QWaZrkzp1ih1G8uZZ+DI9nfLvtD6OdQ==",
+      "requires": {}
     },
     "react-color": {
       "version": "2.19.3",
@@ -23649,7 +23531,8 @@
     "react-onclickoutside": {
       "version": "6.12.2",
       "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
-      "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA=="
+      "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==",
+      "requires": {}
     },
     "react-popper": {
       "version": "2.3.0",
@@ -23699,7 +23582,8 @@
     "react-table": {
       "version": "7.8.0",
       "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
-      "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA=="
+      "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==",
+      "requires": {}
     },
     "react-transition-group": {
       "version": "4.4.5",
@@ -25471,7 +25355,8 @@
     "use-sync-external-store": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
-      "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="
+      "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+      "requires": {}
     },
     "util": {
       "version": "0.11.1",
@@ -26765,7 +26650,8 @@
       "version": "7.5.9",
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
       "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "xtend": {
       "version": "4.0.2",

+ 1 - 1
dashboard/package.json

@@ -7,7 +7,7 @@
     "@loadable/component": "^5.15.2",
     "@material-ui/core": "^4.11.3",
     "@material-ui/lab": "^4.0.0-alpha.61",
-    "@porter-dev/api-contracts": "^0.0.41",
+    "@porter-dev/api-contracts": "^0.0.63",
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",
     "@tanstack/react-query": "^4.13.0",

+ 161 - 0
dashboard/src/components/AWSCostConsent.tsx

@@ -0,0 +1,161 @@
+import React, { useState, useContext, useMemo } from "react";
+import styled from "styled-components";
+
+import { integrationList } from "shared/common";
+import { Context } from "shared/Context";
+import api from "shared/api";
+
+import ProvisionerForm from "components/ProvisionerForm";
+import CloudFormationForm from "components/CloudFormationForm";
+import CredentialsForm from "components/CredentialsForm";
+import Helper from "components/form-components/Helper";
+import Modal from "./porter/Modal";
+import Text from "./porter/Text";
+import Spacer from "./porter/Spacer";
+import Fieldset from "./porter/Fieldset";
+import Checkbox from "./porter/Checkbox";
+import Button from "./porter/Button";
+import ExpandableSection from "./porter/ExpandableSection";
+import Input from "./porter/Input";
+import Link from "./porter/Link";
+import AzureCredentialForm from "components/AzureCredentialForm";
+
+type Props = {
+  setCurrentStep: (step: string) => void;
+  setShowCostConfirmModal: (show: boolean) => void;
+};
+
+const AWSCostConsent: React.FC<Props> = ({
+  setCurrentStep,
+  setShowCostConfirmModal,
+}) => {
+  const { currentProject } = useContext(Context);
+  const [confirmCost, setConfirmCost] = useState("");
+
+  const markStepCostConsent = async () => {
+    try {
+      const res = await api.updateOnboardingStep(
+        "<token>",
+        { step: "cost-consent-complete" },
+        {}
+      );
+    } catch (err) {
+      console.log(err);
+    }
+    try {
+      const res = await api.inviteAdmin(
+        "<token>",
+        {},
+        { project_id: currentProject.id }
+      );
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
+  return (
+    <>
+      <Modal
+        closeModal={() => {
+          setConfirmCost("");
+          setShowCostConfirmModal(false);
+        }}
+      >
+        <Text size={16}>Base AWS cost consent</Text>
+        <Spacer height="15px" />
+        <Text color="helper">
+          Porter will create the underlying infrastructure in your own AWS
+          account. You will be separately charged by AWS for this
+          infrastructure. The cost for this base infrastructure is as follows:
+        </Text>
+        <Spacer y={1} />
+        <ExpandableSection
+          noWrapper
+          expandText="[+] Show details"
+          collapseText="[-] Hide details"
+          Header={<Cost>$315.94 / mo</Cost>}
+          ExpandedSection={
+            <>
+              <Spacer height="15px" />
+              <Fieldset background="#1b1d2688">
+                • Amazon Elastic Kubernetes Service (EKS) = $73/mo
+                <Spacer height="15px" />
+                • Amazon EC2:
+                <Spacer height="15px" />
+                <Tab />+ System workloads: t3.medium instance (2) = $60.74/mo
+                <Spacer height="15px" />
+                <Tab />+ Monitoring workloads: t3.large instance (1) = $60.74/mo
+                <Spacer height="15px" />
+                <Tab />+ Application workloads: t3.xlarge instance (1) =
+                $121.47/mo
+              </Fieldset>
+            </>
+          }
+        />
+        <Spacer y={1} />
+        <Text color="helper">
+          The base AWS infrastructure covers up to 4 vCPU and 16GB of RAM.
+          Separate from the AWS cost, Porter charges based on your resource
+          usage.
+        </Text>
+        <Spacer inline width="5px" />
+        <Spacer y={0.5} />
+        <Link hasunderline to="https://porter.run/pricing" target="_blank">
+          Learn more about our pricing.
+        </Link>
+        <Spacer y={0.5} />
+        <Text color="helper">
+          You can use your AWS credits to pay for the underlying infrastructure,
+          and if you are a startup with less than 5M in funding, you may qualify
+          for our startup program that gives you $10k in credits.
+        </Text>
+        <Spacer y={0.5} />
+        <Link
+          hasunderline
+          to="https://gcpjnf9adme.typeform.com/to/vUg9SDWf"
+          target="_blank"
+        >
+          You can apply here.
+        </Link>
+        <Spacer y={0.5} />
+        <Text color="helper">
+          All AWS resources will be automatically deleted when you delete your
+          Porter project. Please enter the AWS base cost ("315.94") below to
+          proceed:
+        </Text>
+        <Spacer y={1} />
+        <Input
+          placeholder="315.94"
+          value={confirmCost}
+          setValue={setConfirmCost}
+          width="100%"
+          height="40px"
+        />
+        <Spacer y={1} />
+        <Button
+          disabled={confirmCost !== "315.94"}
+          onClick={() => {
+            setShowCostConfirmModal(false);
+            setConfirmCost("");
+            markStepCostConsent();
+            setCurrentStep("credentials");
+          }}
+        >
+          Continue
+        </Button>
+      </Modal>
+    </>
+  );
+};
+
+export default AWSCostConsent;
+
+const Cost = styled.div`
+  font-weight: 600;
+  font-size: 20px;
+`;
+
+const Tab = styled.span`
+  margin-left: 20px;
+  height: 1px;
+`;

+ 164 - 0
dashboard/src/components/AzureCostConsent.tsx

@@ -0,0 +1,164 @@
+import React, { useState, useContext, useMemo } from "react";
+import styled from "styled-components";
+
+import { integrationList } from "shared/common";
+import { Context } from "shared/Context";
+import api from "shared/api";
+
+import ProvisionerForm from "components/ProvisionerForm";
+import CloudFormationForm from "components/CloudFormationForm";
+import CredentialsForm from "components/CredentialsForm";
+import Helper from "components/form-components/Helper";
+import Modal from "./porter/Modal";
+import Text from "./porter/Text";
+import Spacer from "./porter/Spacer";
+import Fieldset from "./porter/Fieldset";
+import Checkbox from "./porter/Checkbox";
+import Button from "./porter/Button";
+import ExpandableSection from "./porter/ExpandableSection";
+import Input from "./porter/Input";
+import Link from "./porter/Link";
+import AzureCredentialForm from "components/AzureCredentialForm";
+
+type Props = {
+  setCurrentStep: (step: string) => void;
+  setShowCostConfirmModal: (show: boolean) => void;
+};
+
+const AzureCostConsent: React.FC<Props> = ({
+  setCurrentStep,
+  setShowCostConfirmModal,
+}) => {
+  const { currentProject } = useContext(Context);
+  const [confirmCost, setConfirmCost] = useState("");
+
+  const markStepCostConsent = async () => {
+    try {
+      const res = await api.updateOnboardingStep(
+        "<token>",
+        { step: "cost-consent-complete" },
+        {}
+      );
+    } catch (err) {
+      console.log(err);
+    }
+    try {
+      const res = await api.inviteAdmin(
+        "<token>",
+        {},
+        { project_id: currentProject.id }
+      );
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
+  return (
+    <>
+      <Modal
+        closeModal={() => {
+          setConfirmCost("");
+          setShowCostConfirmModal(false);
+        }}
+      >
+        <Text size={16}>Base Azure cost consent</Text>
+        <Spacer height="15px" />
+        <Text color="helper">
+          Porter will create the underlying infrastructure in your own Azure
+          account. You will be separately charged by Azure for this
+          infrastructure. The cost for this base infrastructure is as follows:
+        </Text>
+        <Spacer y={1} />
+        <ExpandableSection
+          noWrapper
+          expandText="[+] Show details"
+          collapseText="[-] Hide details"
+          Header={<Cost>$411.72 / mo</Cost>}
+          ExpandedSection={
+            <>
+              <Spacer height="15px" />
+              <Fieldset background="#1b1d2688">
+                • Azure Kubernetes Service (AKS) = $73/mo
+                <Spacer height="15px" />
+                • Amazon EC2:
+                <Spacer height="15px" />
+                <Tab />+ System workloads: Standard_A2_v2 instance (2) =
+                $132.86/mo
+                <Spacer height="15px" />
+                <Tab />+ Monitoring workloads: Standard_A4_v2 instance (1) =
+                $139.43/mo
+                <Spacer height="15px" />
+                <Tab />+ Application workloads: Standard_A2_v2 instance (1) =
+                $66.43/mo
+              </Fieldset>
+            </>
+          }
+        />
+        <Spacer y={1} />
+        <Text color="helper">
+          The base Azure infrastructure covers up to 2 vCPU and 4GB of RAM.
+          Separate from the Azure cost, Porter charges based on your resource
+          usage.
+        </Text>
+        <Spacer inline width="5px" />
+        <Spacer y={0.5} />
+        <Link hasunderline to="https://porter.run/pricing" target="_blank">
+          Learn more about our pricing.
+        </Link>
+        <Spacer y={0.5} />
+        <Text color="helper">
+          You can use your Azure credits to pay for the underlying
+          infrastructure, and if you are a startup with less than 5M in funding,
+          you may qualify for our startup program that gives you $10k in
+          credits.
+        </Text>
+        <Spacer y={0.5} />
+        <Link
+          hasunderline
+          to="https://gcpjnf9adme.typeform.com/to/vUg9SDWf"
+          target="_blank"
+        >
+          You can apply here.
+        </Link>
+        <Spacer y={0.5} />
+        <Text color="helper">
+          All Azure resources will be automatically deleted when you delete your
+          Porter project. Please enter the Azure base cost ("411.72") below to
+          proceed:
+        </Text>
+        <Spacer y={1} />
+        <Input
+          placeholder="411.72"
+          value={confirmCost}
+          setValue={setConfirmCost}
+          width="100%"
+          height="40px"
+        />
+        <Spacer y={1} />
+        <Button
+          disabled={confirmCost !== "411.72"}
+          onClick={() => {
+            setShowCostConfirmModal(false);
+            setConfirmCost("");
+            markStepCostConsent();
+            setCurrentStep("credentials");
+          }}
+        >
+          Continue
+        </Button>
+      </Modal>
+    </>
+  );
+};
+
+export default AzureCostConsent;
+
+const Cost = styled.div`
+  font-weight: 600;
+  font-size: 20px;
+`;
+
+const Tab = styled.span`
+  margin-left: 20px;
+  height: 1px;
+`;

+ 256 - 0
dashboard/src/components/AzureCredentialForm.tsx

@@ -0,0 +1,256 @@
+import React, { useEffect, useState, useContext, useMemo } from "react";
+import styled from "styled-components";
+import { v4 as uuidv4 } from "uuid";
+
+import api from "shared/api";
+import azure from "assets/azure.png";
+
+import { Context } from "shared/Context";
+
+import Text from "./porter/Text";
+import Spacer from "./porter/Spacer";
+import InputRow from "./form-components/InputRow";
+import SaveButton from "./SaveButton";
+import Fieldset from "./porter/Fieldset";
+import Input from "./porter/Input";
+import Button from "./porter/Button";
+import DocsHelper from "./DocsHelper";
+import Error from "./porter/Error";
+import Step from "./porter/Step";
+import Link from "./porter/Link";
+import Container from "./porter/Container";
+
+type Props = {
+  goBack: () => void;
+  proceed: (id: string) => void;
+};
+
+const AzureCredentialForm: React.FC<Props> = ({ goBack, proceed }) => {
+  const { currentProject } = useContext(Context);
+  const [clientId, setClientId] = useState("");
+  const [servicePrincipalKey, setServicePrincipalKey] = useState("");
+  const [tenantId, setTenantId] = useState("");
+  const [subscriptionId, setSubscriptionId] = useState("");
+  const [roleStatus, setRoleStatus] = useState("");
+  const [errorMessage, setErrorMessage] = useState(undefined);
+  const [isLoading, setIsLoading] = useState(false);
+
+  const saveCredentials = () => {
+    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 }) => {
+        setIsLoading(false);
+        proceed(data.id);
+      })
+      .catch((err) => {
+        console.error(err);
+        setErrorMessage(err);
+        setIsLoading(false);
+      });
+  };
+
+  const renderContent = () => {
+    return (
+      <>
+        <Spacer y={1} />
+        <Fieldset>
+          <Text size={16}>
+            Create an Azure Service Principal and input credentials
+          </Text>
+          <Spacer height="15px" />
+          <Text color="helper">
+            Provide the credentials for an Azure Service Principal authorized on
+            your Azure subscription.
+          </Text>
+          <Spacer y={1} />
+          <Input
+            label={<Flex>Subscription ID</Flex>}
+            value={subscriptionId}
+            setValue={(e) => {
+              setSubscriptionId(e.trim());
+            }}
+            placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+            width="100%"
+          />
+          <Spacer y={1} />
+          <Input
+            label={<Flex>Client ID</Flex>}
+            value={clientId}
+            setValue={(e) => {
+              setClientId(e.trim());
+            }}
+            placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+            width="100%"
+          />
+          <Spacer y={1} />
+          <Input
+            label={<Flex>Tenant ID</Flex>}
+            value={tenantId}
+            setValue={(e) => {
+              setTenantId(e.trim());
+            }}
+            placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+            width="100%"
+          />
+          <Spacer y={1} />
+          <Input
+            label={<Flex>Client Secret</Flex>}
+            value={servicePrincipalKey}
+            setValue={(e) => {
+              setServicePrincipalKey(e.trim());
+            }}
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
+          />
+        </Fieldset>
+        <Spacer y={1} />
+        <Button
+          onClick={() => {
+            saveCredentials();
+          }}
+          status={
+            errorMessage ? (
+              <Error
+                message={errorMessage}
+                ctaText="Troubleshooting steps"
+                errorModalContents={
+                  <>
+                    <Text size={16}>Granting Porter access to Azure</Text>
+                    <Spacer y={1} />
+                    <Text color="helper">
+                      Porter needs access to your Azure subscription in order to
+                      create infrastructure. You can grant Porter access to
+                      Azure by following these steps:
+                    </Text>
+                    <Spacer y={1} />
+                    <Step number={1}>
+                      <Link
+                        to="https://aws.amazon.com/resources/create-account/"
+                        target="_blank"
+                      >
+                        Create an AWS account
+                      </Link>
+                      <Spacer inline width="5px" />
+                      if you don't already have one.
+                    </Step>
+                    <Spacer y={1} />
+                    <Step number={2}>
+                      Once you are logged in to your AWS account,
+                      <Spacer inline width="5px" />
+                      <Link
+                        to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+                        target="_blank"
+                      >
+                        copy your account ID
+                      </Link>
+                      .
+                    </Step>
+                    <Spacer y={1} />
+                    <Step number={3}>
+                      Fill in your account ID on Porter and select "Grant
+                      permissions".
+                    </Step>
+                    <Spacer y={1} />
+                    <Step number={4}>
+                      After being redirected to AWS, select "Create stack" on
+                      the AWS console.
+                    </Step>
+                    <Spacer y={1} />
+                    <Step number={5}>
+                      Wait until the stack status has changed from
+                      "CREATE_IN_PROGRESS" to "CREATE_COMPLETE".
+                    </Step>
+                    <Spacer y={1} />
+                    <Step number={6}>
+                      Return to Porter and select "Continue".
+                    </Step>
+                  </>
+                }
+              />
+            ) : (
+              roleStatus
+            )
+          }
+        >
+          Continue
+        </Button>
+      </>
+    );
+  };
+
+  return (
+    <>
+      <Container row>
+        <BackButton width="140px" onClick={goBack}>
+          <i className="material-icons">first_page</i>
+          Select cloud
+        </BackButton>
+        <Spacer x={1} inline />
+        <Img src={azure} />
+        <Text size={16}>Grant Azure permissions</Text>
+      </Container>
+      <Spacer y={1} />
+      <Text color="helper">
+        Grant Porter permissions to create infrastructure in your Azure
+        subscription.
+      </Text>
+      {renderContent()}
+    </>
+  );
+};
+
+export default AzureCredentialForm;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  > i {
+    margin-left: 10px;
+    font-size: 16px;
+    cursor: pointer;
+  }
+`;
+
+const Img = styled.img`
+  height: 18px;
+  margin-right: 15px;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 13px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+    margin-left: -2px;
+  }
+`;

+ 387 - 0
dashboard/src/components/AzureProvisionerSettings.tsx

@@ -0,0 +1,387 @@
+import React, { useEffect, useState, useContext } from "react";
+import styled from "styled-components";
+import { RouteComponentProps, withRouter } from "react-router";
+
+import { OFState } from "main/home/onboarding/state";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { pushFiltered } from "shared/routing";
+
+import SelectRow from "components/form-components/SelectRow";
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
+import InputRow from "./form-components/InputRow";
+import {
+  Contract,
+  EnumKubernetesKind,
+  EnumCloudProvider,
+  Cluster,
+  AKS,
+  AKSNodePool,
+  NodePoolType,
+} from "@porter-dev/api-contracts";
+import { ClusterType } from "shared/types";
+import Button from "./porter/Button";
+import Error from "./porter/Error";
+import Spacer from "./porter/Spacer";
+import Step from "./porter/Step";
+import Link from "./porter/Link";
+import Text from "./porter/Text";
+
+const locationOptions = [
+  { value: "eastus", label: "East US" },
+  { value: "westus2", label: "West US 2" },
+  { value: "westus3", label: "West US 3" },
+];
+
+const machineTypeOptions = [
+  { value: "Standard_A2_v2", label: "Standard_A2_v2" },
+  { value: "Standard_A4_v2", label: "Standard_A4_v2" },
+];
+
+const clusterVersionOptions = [{ value: "v1.24.9", label: "v1.24.9" }];
+
+type Props = RouteComponentProps & {
+  selectedClusterVersion?: Contract;
+  provisionerError?: string;
+  credentialId: string;
+  clusterId?: number;
+};
+
+const AzureProvisionerSettings: React.FC<Props> = (props) => {
+  const {
+    user,
+    currentProject,
+    currentCluster,
+    setCurrentCluster,
+    setShouldRefreshClusters,
+    setHasFinishedOnboarding,
+  } = useContext(Context);
+  const [createStatus, setCreateStatus] = useState("");
+  const [clusterName, setClusterName] = useState("");
+  const [azureLocation, setAzureLocation] = useState("eastus");
+  const [machineType, setMachineType] = useState("Standard_A2_v2");
+  const [isExpanded, setIsExpanded] = useState(false);
+  const [minInstances, setMinInstances] = useState(1);
+  const [maxInstances, setMaxInstances] = useState(10);
+  const [cidrRange, setCidrRange] = useState("10.78.0.0/16");
+  const [clusterVersion, setClusterVersion] = useState("v1.24.9");
+  const [isReadOnly, setIsReadOnly] = useState(false);
+  const [errorMessage, setErrorMessage] = useState<string>(undefined);
+  const [isClicked, setIsClicked] = useState(false);
+
+  const markStepStarted = async (step: string) => {
+    try {
+      await api.updateOnboardingStep("<token>", { step }, {});
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
+  const getStatus = () => {
+    if (isReadOnly && props.provisionerError == "") {
+      return "Provisioning is still in progress...";
+    } else if (errorMessage) {
+      return (
+        <Error
+          message={errorMessage}
+          ctaText={
+            errorMessage !== DEFAULT_ERROR_MESSAGE
+              ? "Troubleshooting steps"
+              : null
+          }
+          errorModalContents={null}
+        />
+      );
+    }
+    return undefined;
+  };
+  const isDisabled = () => {
+    return (
+      !user.email.endsWith("porter.run") &&
+      ((!clusterName && true) ||
+        (isReadOnly && props.provisionerError === "") ||
+        props.provisionerError === "" ||
+        currentCluster.status === "UPDATING" ||
+        isClicked)
+    );
+  };
+  const createCluster = async () => {
+    setIsClicked(true);
+    var data = new Contract({
+      cluster: new Cluster({
+        projectId: currentProject.id,
+        kind: EnumKubernetesKind.AKS,
+        cloudProvider: EnumCloudProvider.AZURE,
+        cloudProviderCredentialsId: "",
+        kindValues: {
+          case: "aksKind",
+          value: new AKS({
+            clusterName: clusterName,
+            clusterVersion: clusterVersion || "v1.24.9",
+            cidrRange: cidrRange || "10.78.0.0/16",
+            location: azureLocation,
+            nodePools: [
+              new AKSNodePool({
+                instanceType: "Standard_A2_v2",
+                minInstances: 1,
+                maxInstances: 3,
+                nodePoolType: NodePoolType.SYSTEM,
+                mode: "User",
+              }),
+              new AKSNodePool({
+                instanceType: "Standard_A4_v2",
+                minInstances: 1,
+                maxInstances: 3,
+                nodePoolType: NodePoolType.MONITORING,
+                mode: "User",
+              }),
+              new AKSNodePool({
+                instanceType: machineType,
+                minInstances: minInstances || 1,
+                maxInstances: maxInstances || 10,
+                nodePoolType: NodePoolType.APPLICATION,
+                mode: "User",
+              }),
+            ],
+          }),
+        },
+      }),
+    });
+
+    if (props.clusterId) {
+      data["cluster"]["clusterId"] = props.clusterId;
+    }
+
+    try {
+      setIsReadOnly(true);
+      setErrorMessage(undefined);
+
+      if (!props.clusterId) {
+        markStepStarted("provisioning-started");
+      }
+
+      const res = await api.createContract("<token>", data, {
+        project_id: currentProject.id,
+      });
+
+      // Only refresh and set clusters on initial create
+      // if (!props.clusterId) {
+      setShouldRefreshClusters(true);
+      api
+        .getClusters("<token>", {}, { id: currentProject.id })
+        .then(({ data }) => {
+          data.forEach((cluster: ClusterType) => {
+            if (cluster.id === res.data.contract_revision?.cluster_id) {
+              // setHasFinishedOnboarding(true);
+              setCurrentCluster(cluster);
+              OFState.actions.goTo("clean_up");
+              pushFiltered(props, "/cluster-dashboard", ["project_id"], {
+                cluster: cluster.name,
+              });
+            }
+          });
+        })
+        .catch((err) => {
+          console.error(err);
+        });
+      // }
+      setErrorMessage(undefined);
+    } catch (err) {
+      const errMessage = err.response.data.error.replace("unknown: ", "");
+      // hacky, need to standardize error contract with backend
+      setIsClicked(false);
+      setErrorMessage(DEFAULT_ERROR_MESSAGE);
+    } finally {
+      setIsReadOnly(false);
+      setIsClicked(false);
+    }
+  };
+
+  useEffect(() => {
+    setIsReadOnly(
+      props.clusterId &&
+        (currentCluster.status === "UPDATING" ||
+          currentCluster.status === "UPDATING_UNAVAILABLE")
+    );
+    setClusterName(
+      `${currentProject.name}-cluster-${Math.random()
+        .toString(36)
+        .substring(2, 8)}`
+    );
+  }, []);
+
+  useEffect(() => {
+    const contract = props.selectedClusterVersion as any;
+    if (contract?.cluster) {
+      contract.cluster.aksKind.nodePools.map((nodePool: any) => {
+        if (nodePool.nodePoolType === "NODE_POOL_TYPE_APPLICATION") {
+          setMachineType(nodePool.instanceType);
+          setMinInstances(nodePool.minInstances);
+          setMaxInstances(nodePool.maxInstances);
+        }
+      });
+      setCreateStatus("");
+      setClusterName(contract.cluster.aksKind.clusterName);
+      setAzureLocation(contract.cluster.aksKind.location);
+      setClusterVersion(contract.cluster.aksKind.clusterVersion);
+      setCidrRange(contract.cluster.aksKind.cidrRange);
+    }
+  }, [props.selectedClusterVersion]);
+
+  const renderForm = () => {
+    // Render simplified form if initial create
+    if (!props.clusterId) {
+      return (
+        <>
+          <Text size={16}>Select an Azure location</Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            Porter will automatically provision your infrastructure in the
+            specified location.
+          </Text>
+          <Spacer height="10px" />
+          <SelectRow
+            options={locationOptions}
+            width="350px"
+            disabled={isReadOnly}
+            value={azureLocation}
+            scrollBuffer={true}
+            dropdownMaxHeight="240px"
+            setActiveValue={setAzureLocation}
+            label="📍 Azure location"
+          />
+          <SelectRow
+            options={machineTypeOptions}
+            width="350px"
+            disabled={isReadOnly}
+            value={machineType}
+            scrollBuffer={true}
+            dropdownMaxHeight="240px"
+            setActiveValue={setMachineType}
+            label="Machine type"
+          />
+          <InputRow
+            width="350px"
+            type="string"
+            disabled={isReadOnly}
+            value={cidrRange}
+            setValue={(x: string) => setCidrRange(x)}
+            label="VPC CIDR range"
+            placeholder="ex: 10.78.0.0/16"
+          />
+        </>
+      );
+    }
+
+    // If settings, update full form
+    return (
+      <>
+        <Heading isAtTop>AKS configuration</Heading>
+        <SelectRow
+          options={locationOptions}
+          width="350px"
+          disabled={isReadOnly || true}
+          value={azureLocation}
+          scrollBuffer={true}
+          dropdownMaxHeight="240px"
+          setActiveValue={setAzureLocation}
+          label="📍 Azure location"
+        />
+        {user?.isPorterUser && (
+          <Heading>
+            <ExpandHeader
+              onClick={() => setIsExpanded(!isExpanded)}
+              isExpanded={isExpanded}
+            >
+              <i className="material-icons">arrow_drop_down</i>
+              Advanced settings
+            </ExpandHeader>
+          </Heading>
+        )}
+        {isExpanded && (
+          <>
+            <SelectRow
+              options={clusterVersionOptions}
+              width="350px"
+              disabled={isReadOnly}
+              value={clusterVersion}
+              scrollBuffer={true}
+              dropdownMaxHeight="240px"
+              setActiveValue={setClusterVersion}
+              label="Cluster version"
+            />
+            <SelectRow
+              options={machineTypeOptions}
+              width="350px"
+              disabled={isReadOnly}
+              value={machineType}
+              scrollBuffer={true}
+              dropdownMaxHeight="240px"
+              setActiveValue={setMachineType}
+              label="Machine type"
+            />
+            <InputRow
+              width="350px"
+              type="number"
+              disabled={isReadOnly}
+              value={maxInstances}
+              setValue={(x: number) => setMaxInstances(x)}
+              label="Maximum number of application EC2 instances"
+              placeholder="ex: 1"
+            />
+            <InputRow
+              width="350px"
+              type="string"
+              disabled={isReadOnly}
+              value={cidrRange}
+              setValue={(x: string) => setCidrRange(x)}
+              label="VPC CIDR range"
+              placeholder="ex: 10.78.0.0/16"
+            />
+          </>
+        )}
+      </>
+    );
+  };
+
+  return (
+    <>
+      <StyledForm>{renderForm()}</StyledForm>
+      <Button
+        disabled={isDisabled()}
+        onClick={createCluster}
+        status={getStatus()}
+      >
+        Provision
+      </Button>
+    </>
+  );
+};
+
+export default withRouter(AzureProvisionerSettings);
+
+const ExpandHeader = styled.div<{ isExpanded: boolean }>`
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  > i {
+    margin-right: 7px;
+    margin-left: -7px;
+    transform: ${(props) =>
+      props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
+  }
+`;
+
+const StyledForm = styled.div`
+  position: relative;
+  padding: 30px 30px 25px;
+  border-radius: 5px;
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid #494b4f;
+  font-size: 13px;
+  margin-bottom: 30px;
+`;
+
+const DEFAULT_ERROR_MESSAGE =
+  "An error occurred while provisioning your infrastructure. Please try again.";

+ 72 - 104
dashboard/src/components/ProvisionerFlow.tsx

@@ -18,20 +18,27 @@ import Button from "./porter/Button";
 import ExpandableSection from "./porter/ExpandableSection";
 import Input from "./porter/Input";
 import Link from "./porter/Link";
+import AzureCredentialForm from "components/AzureCredentialForm";
+import AWSCostConsent from "./AWSCostConsent";
+import AzureCostConsent from "./AzureCostConsent";
 
 const providers = ["aws", "gcp", "azure"];
 
-type Props = {
-};
+type Props = {};
 
-const ProvisionerFlow: React.FC<Props> = ({
-}) => {
-  const { usage, hasBillingEnabled, currentProject } = useContext(Context);
+const ProvisionerFlow: React.FC<Props> = ({}) => {
+  const {
+    usage,
+    hasBillingEnabled,
+    currentProject,
+    featurePreview,
+  } = useContext(Context);
   const [currentStep, setCurrentStep] = useState("cloud");
   const [credentialId, setCredentialId] = useState("");
   const [showCostConfirmModal, setShowCostConfirmModal] = useState(false);
   const [confirmCost, setConfirmCost] = useState("");
   const [useCloudFormationForm, setUseCloudFormationForm] = useState(true);
+  const [selectedProvider, setSelectedProvider] = useState("");
 
   const isUsageExceeded = useMemo(() => {
     if (!hasBillingEnabled) {
@@ -59,137 +66,98 @@ const ProvisionerFlow: React.FC<Props> = ({
     } catch (err) {
       console.log(err);
     }
-  }
+  };
 
   if (currentStep === "cloud") {
     return (
       <>
         <StyledProvisionerFlow>
-          <Helper>
-            Select your hosting backend:
-          </Helper>
+          <Helper>Select your hosting backend:</Helper>
           <BlockList>
             {providers.map((provider: string, i: number) => {
               let providerInfo = integrationList[provider];
               return (
                 <Block
                   key={i}
-                  disabled={isUsageExceeded || provider === "gcp" || provider === "azure"}
+                  disabled={
+                    isUsageExceeded ||
+                    (provider === "azure" && !featurePreview) ||
+                    provider === "gcp"
+                  }
                   onClick={() => {
-                    if (!(isUsageExceeded || provider === "gcp" || provider === "azure")) {
+                    if (
+                      !(
+                        isUsageExceeded ||
+                        (provider === "azure" && !featurePreview) ||
+                        provider === "gcp"
+                      )
+                    ) {
+                      setSelectedProvider(provider);
                       setShowCostConfirmModal(true);
                     }
                   }}
                 >
                   <Icon src={providerInfo.icon} />
                   <BlockTitle>{providerInfo.label}</BlockTitle>
-                  <BlockDescription>{providerInfo.tagline || "Hosted in your own cloud"}</BlockDescription>
+                  <BlockDescription>
+                    {providerInfo.tagline || "Hosted in your own cloud"}
+                  </BlockDescription>
                 </Block>
               );
             })}
           </BlockList>
         </StyledProvisionerFlow>
-        {showCostConfirmModal && (
-          <Modal closeModal={() => {
-            setConfirmCost("");
-            setShowCostConfirmModal(false);
-          }}>
-            <Text size={16}>
-              Base AWS cost consent
-            </Text>
-            <Spacer height="15px" />
-            <Text color="helper">
-              Porter will create the underlying infrastructure in your own AWS account. You will be separately charged by AWS for this infrastructure. The cost for this base infrastructure is as follows:
-            </Text>
-            <Spacer y={1} />
-            <ExpandableSection
-              noWrapper
-              expandText="[+] Show details"
-              collapseText="[-] Hide details"
-              Header={
-                <Cost>$315.94 / mo</Cost>
-              }
-              ExpandedSection={
-                <>
-                  <Spacer height="15px" />
-                  <Fieldset background="#1b1d2688">
-                    • Amazon Elastic Kubernetes Service (EKS) = $73/mo
-                    <Spacer height="15px" />
-                    • Amazon EC2:
-                    <Spacer height="15px" />
-                    <Tab />+ System workloads: t3.medium instance (2) = $60.74/mo
-                    <Spacer height="15px" />
-                    <Tab />+ Monitoring workloads: t3.large instance (1) = $60.74/mo
-                    <Spacer height="15px" />
-                    <Tab />+ Application workloads: t3.xlarge instance (1) = $121.47/mo
-                  </Fieldset>
-                </>
-              }
+        {showCostConfirmModal &&
+          ((selectedProvider === "aws" && (
+            <AWSCostConsent
+              setCurrentStep={setCurrentStep}
+              setShowCostConfirmModal={setShowCostConfirmModal}
             />
-            <Spacer y={1} />
-            <Text color="helper">
-              The base AWS infrastructure covers up to 4 vCPU and 16GB of RAM. Separate from the AWS cost, Porter charges based on your resource usage.
-            </Text>
-            <Spacer inline width="5px" />
-            <Spacer y={0.5} />
-            <Link hasunderline to="https://porter.run/pricing" target="_blank">
-              Learn more about our pricing.
-            </Link>
-            <Spacer y={0.5} />
-            <Text color="helper">
-              You can use your AWS credits to pay for the underlying infrastructure, and if you are a startup with less than 5M in funding, you may qualify for our startup program that gives you $10k in credits.
-            </Text>
-            <Spacer y={0.5} />
-            <Link hasunderline to="https://gcpjnf9adme.typeform.com/to/vUg9SDWf" target="_blank">
-              You can apply here.
-            </Link>
-            <Spacer y={0.5} />
-            <Text color="helper">
-              All AWS resources will be automatically deleted when you delete your Porter project. Please enter the AWS base cost ("315.94") below to proceed:
-            </Text>
-            <Spacer y={1} />
-            <Input placeholder="315.94" value={confirmCost} setValue={setConfirmCost} width="100%" height="40px" />
-            <Spacer y={1} />
-            <Button
-              disabled={confirmCost !== "315.94"}
-              onClick={() => {
-                setShowCostConfirmModal(false);
-                setConfirmCost("");
-                markStepCostConsent();
-                setCurrentStep("credentials");
-              }}
-            >
-              Continue
-            </Button>
-          </Modal>
-        )}
+          )) ||
+            (selectedProvider === "azure" && (
+              <AzureCostConsent
+                setCurrentStep={setCurrentStep}
+                setShowCostConfirmModal={setShowCostConfirmModal}
+              />
+            )))}
       </>
     );
   } else if (currentStep === "credentials") {
-    return useCloudFormationForm ? (
-      <CloudFormationForm
-        goBack={() => setCurrentStep("cloud")}
-        proceed={(id) => {
-          setCredentialId(id);
-          setCurrentStep("cluster");
-        }}
-        switchToCredentialFlow={() => setUseCloudFormationForm(false)}
-      />
-    ) : (
-      <CredentialsForm
-        goBack={() => setCurrentStep("cloud")}
-        proceed={(id) => {
-          setCredentialId(id);
-          setCurrentStep("cluster");
-        }}
-      />
+    return (
+      (selectedProvider === "aws" &&
+        (useCloudFormationForm ? (
+          <CloudFormationForm
+            goBack={() => setCurrentStep("cloud")}
+            proceed={(id) => {
+              setCredentialId(id);
+              setCurrentStep("cluster");
+            }}
+            switchToCredentialFlow={() => setUseCloudFormationForm(false)}
+          />
+        ) : (
+          <CredentialsForm
+            goBack={() => setCurrentStep("cloud")}
+            proceed={(id) => {
+              setCredentialId(id);
+              setCurrentStep("cluster");
+            }}
+          />
+        ))) ||
+      (selectedProvider === "azure" && (
+        <AzureCredentialForm
+          goBack={() => setCurrentStep("cloud")}
+          proceed={() => {
+            setCurrentStep("cluster");
+          }}
+        />
+      ))
     );
   } else if (currentStep === "cluster") {
     return (
       <ProvisionerForm
         goBack={() => setCurrentStep("credentials")}
         credentialId={credentialId}
-        useAssumeRole={useCloudFormationForm}
+        provider={selectedProvider}
       />
     );
   }
@@ -285,4 +253,4 @@ const Block = styled.div<{ disabled?: boolean }>`
 
 const StyledProvisionerFlow = styled.div`
   margin-top: -24px;
-`;
+`;

+ 43 - 20
dashboard/src/components/ProvisionerForm.tsx

@@ -2,6 +2,7 @@ import React, { useEffect, useState, useContext } from "react";
 import styled from "styled-components";
 
 import aws from "assets/aws.png";
+import azure from "assets/azure.png";
 
 import Heading from "components/form-components/Heading";
 import Helper from "./form-components/Helper";
@@ -10,37 +11,59 @@ import ProvisionerSettingsOld from "./ProvisionerSettingsOld";
 import Text from "./porter/Text";
 import Spacer from "./porter/Spacer";
 import Container from "./porter/Container";
+import AzureProvisionerSettings from "./AzureProvisionerSettings";
 
 type Props = {
   goBack: () => void;
   credentialId: string;
-  useAssumeRole?: boolean;
+  provider: string;
 };
 
 const ProvisionerForm: React.FC<Props> = ({
   goBack,
   credentialId,
-  useAssumeRole,
+  provider,
 }) => {
   return (
     <>
-      <Container row>
-        <BackButton width="155px" onClick={goBack}>
-          <i className="material-icons">first_page</i>
-          Set credentials
-        </BackButton>
-        <Spacer inline width="17px" />
-        <Img src={aws} />
-        <Text size={16}>
-          Configure settings
-        </Text>
-      </Container>
-      <Spacer y={1} />
-      <Text color="helper">
-        Configure settings for your AWS environment.
-      </Text>
-      <Spacer y={1} />
-      <ProvisionerSettings credentialId={credentialId} />
+      {provider === "aws" && (
+        <>
+          <Container row>
+            <BackButton width="155px" onClick={goBack}>
+              <i className="material-icons">first_page</i>
+              Set credentials
+            </BackButton>
+            <Spacer inline width="17px" />
+            <Img src={aws} />
+            <Text size={16}>Configure settings</Text>
+          </Container>
+          <Spacer y={1} />
+          <Text color="helper">
+            Configure settings for your AWS environment.
+          </Text>
+          <Spacer y={1} />
+          <ProvisionerSettings credentialId={credentialId} />
+        </>
+      )}
+      {provider === "azure" && (
+        <>
+          <Container row>
+            <BackButton width="155px" onClick={goBack}>
+              <i className="material-icons">first_page</i>
+              Set credentials
+            </BackButton>
+            <Spacer inline width="17px" />
+            <Img src={azure} />
+            <Text size={16}>Configure settings</Text>
+          </Container>
+          <Spacer y={1} />
+          <Text color="helper">
+            Configure settings for your Azure environment.
+          </Text>
+          <Spacer y={1} />
+          <AzureProvisionerSettings credentialId={credentialId} />
+        </>
+      )}
     </>
   );
 };
@@ -77,4 +100,4 @@ const BackButton = styled.div`
     margin-right: 6px;
     margin-left: -2px;
   }
-`;
+`;

+ 25 - 9
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -21,6 +21,7 @@ import ClusterSettingsModal from "./ClusterSettingsModal";
 
 import Loading from "components/Loading";
 import Spacer from "components/porter/Spacer";
+import AzureProvisionerSettings from "components/AzureProvisionerSettings";
 
 type TabEnum =
   | "nodes"
@@ -45,6 +46,7 @@ export const Dashboard: React.FunctionComponent = () => {
   const [provisionFailureReason, setProvisionFailureReason] = useState("");
   const [ingressIp, setIngressIp] = useState(null);
   const [ingressError, setIngressError] = useState(null);
+  const [cloudProvider, setCloudProvider] = useState("azure");
 
   const context = useContext(Context);
   const renderTab = () => {
@@ -64,17 +66,30 @@ export const Dashboard: React.FunctionComponent = () => {
       case "namespaces":
         return <NamespaceList />;
       case "configuration":
+        console.log(context.currentCluster);
         return (
           <>
             <Br />
-            <ProvisionerSettings
-              selectedClusterVersion={selectedClusterVersion}
-              provisionerError={provisionFailureReason}
-              clusterId={context.currentCluster.id}
-              credentialId={
-                context.currentCluster.cloud_provider_credential_identifier
-              }
-            />
+            {context.currentCluster.cloud_provider == "AWS" && (
+              <ProvisionerSettings
+                selectedClusterVersion={selectedClusterVersion}
+                provisionerError={provisionFailureReason}
+                clusterId={context.currentCluster.id}
+                credentialId={
+                  context.currentCluster.cloud_provider_credential_identifier
+                }
+              />
+            )}
+            {context.currentCluster.cloud_provider == "Azure" && (
+              <AzureProvisionerSettings
+                selectedClusterVersion={selectedClusterVersion}
+                provisionerError={provisionFailureReason}
+                clusterId={context.currentCluster.id}
+                credentialId={
+                  context.currentCluster.cloud_provider_credential_identifier
+                }
+              />
+            )}
           </>
         );
       default:
@@ -83,10 +98,11 @@ export const Dashboard: React.FunctionComponent = () => {
   };
 
   useEffect(() => {
+    ``;
     if (
       context.currentCluster.status !== "UPDATING_UNAVAILABLE" &&
       !tabOptions.find((tab) => tab.value === "nodes")
-    ) {  
+    ) {
       if (!context.currentProject?.capi_provisioner_enabled) {
         tabOptions.unshift({ label: "Namespaces", value: "namespaces" });
         tabOptions.unshift({ label: "Metrics", value: "metrics" });

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

@@ -14,6 +14,7 @@ export interface ClusterType {
   preview_envs_enabled?: boolean;
   cloud_provider_credential_identifier?: string;
   status?: string;
+  cloud_provider: string;
 }
 
 export interface DetailedClusterType extends ClusterType {

+ 1 - 1
go.mod

@@ -76,7 +76,7 @@ require (
 	github.com/honeycombio/otel-launcher-go v0.2.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.0.61
+	github.com/porter-dev/api-contracts v0.0.63
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d

+ 2 - 2
go.sum

@@ -1490,8 +1490,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.0.61 h1:s/0A3YoPIPvcWwjkN3XfmN/Q4+iR0QN+HBTVcLg+DrY=
-github.com/porter-dev/api-contracts v0.0.61/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
+github.com/porter-dev/api-contracts v0.0.63 h1:ZC2uURhfPmuknnNm9atM+7y3yW6YBCaR1vRQ9EvHCQc=
+github.com/porter-dev/api-contracts v0.0.63/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
 github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935 h1:hfb3nt3AJXIBbevu6ARTg9SdOkMP6WLbKBiG5hT5rcc=
 github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 1 - 0
go.work.sum

@@ -135,6 +135,7 @@ github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/5
 github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ=
 github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4=
 github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
+github.com/porter-dev/api-contracts v0.0.63/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
 github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
 github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck=

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "porter",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}