Bläddra i källkod

[POR-1601] New UI for Preflight Checks for AWS (#3516)

Co-authored-by: Feroze Mohideen <feroze@porter.run>
sdess09 2 år sedan
förälder
incheckning
abf49c1d57

+ 1 - 1
api/server/handlers/project_integration/preflight_check.go

@@ -53,7 +53,7 @@ func (p *CreatePreflightCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 
 	if cloudValues.PreflightValues != nil {
-		if cloudValues.CloudProvider == porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_GCP {
+		if cloudValues.CloudProvider == porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_GCP || cloudValues.CloudProvider == porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_AWS {
 			input.PreflightValues = cloudValues.PreflightValues
 		}
 	}

+ 13 - 13
dashboard/package-lock.json

@@ -13,7 +13,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.99",
+        "@porter-dev/api-contracts": "^0.0.100",
         "@react-spring/web": "^9.6.1",
         "@sentry/react": "^6.13.2",
         "@sentry/tracing": "^6.13.2",
@@ -1952,9 +1952,9 @@
       }
     },
     "node_modules/@bufbuild/protobuf": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.0.tgz",
-      "integrity": "sha512-G372ods0pLt46yxVRsnP/e2btVPuuzArcMPFpIDeIwiGPuuglEs9y75iG0HMvZgncsj5TvbYRWqbVyOe3PLCWQ=="
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.1.tgz",
+      "integrity": "sha512-BUyJWutgP2S8K/1NphOJokuwDckXS4qI2T1pGZAlkFdZchWae3jm6fCdkcGbLlM1QLOcNFFePd+7Feo4BYGrJQ=="
     },
     "node_modules/@discoveryjs/json-ext": {
       "version": "0.5.7",
@@ -2454,9 +2454,9 @@
       }
     },
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.0.99",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.99.tgz",
-      "integrity": "sha512-boropiMEHIXJLTKxmO6689GhIMiTC95JMkL1ouFxn2mkiT6DPcJ08UfD5tKohUMYGhgQNJceBQ1biPVjn5nqJQ==",
+      "version": "0.0.100",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.100.tgz",
+      "integrity": "sha512-Y17fzm6HHmClFnMEWgwr178wZBTOuF17903/2icG/u4CA9JhtVgH6QvSzYcJ/Eu0kX+7pXm6pw24bxagFIeivA==",
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
       }
@@ -16595,9 +16595,9 @@
       }
     },
     "@bufbuild/protobuf": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.0.tgz",
-      "integrity": "sha512-G372ods0pLt46yxVRsnP/e2btVPuuzArcMPFpIDeIwiGPuuglEs9y75iG0HMvZgncsj5TvbYRWqbVyOe3PLCWQ=="
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.1.tgz",
+      "integrity": "sha512-BUyJWutgP2S8K/1NphOJokuwDckXS4qI2T1pGZAlkFdZchWae3jm6fCdkcGbLlM1QLOcNFFePd+7Feo4BYGrJQ=="
     },
     "@discoveryjs/json-ext": {
       "version": "0.5.7",
@@ -16943,9 +16943,9 @@
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "@porter-dev/api-contracts": {
-      "version": "0.0.99",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.99.tgz",
-      "integrity": "sha512-boropiMEHIXJLTKxmO6689GhIMiTC95JMkL1ouFxn2mkiT6DPcJ08UfD5tKohUMYGhgQNJceBQ1biPVjn5nqJQ==",
+      "version": "0.0.100",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.100.tgz",
+      "integrity": "sha512-Y17fzm6HHmClFnMEWgwr178wZBTOuF17903/2icG/u4CA9JhtVgH6QvSzYcJ/Eu0kX+7pXm6pw24bxagFIeivA==",
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"
       }

+ 1 - 1
dashboard/package.json

@@ -8,7 +8,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.99",
+    "@porter-dev/api-contracts": "^0.0.100",
     "@react-spring/web": "^9.6.1",
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",

+ 2 - 39
dashboard/src/components/GCPCredentialsForm.tsx

@@ -12,9 +12,6 @@ import Text from "components/porter/Text";
 import Button from "components/porter/Button";
 import Spacer from "./porter/Spacer";
 import Container from "./porter/Container";
-import PreflightChecks from "./PreflightChecks";
-import { EnumCloudProvider, GKENetwork, GKEPreflightValues, PreflightCheckRequest } from "@porter-dev/api-contracts";
-
 
 
 type Props = {
@@ -31,8 +28,7 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
   const [errorMessage, setErrorMessage] = useState("");
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
   const [gcpCloudProviderCredentialID, setGCPCloudProviderCredentialId] = useState<string>("")
-  const [preFlightData, setPreflightData] = useState(null)
-  const [preflightFailed, setPreflightFailed] = useState<boolean>(true)
+
 
   useEffect(() => {
     setDetected(undefined);
@@ -83,25 +79,6 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
       }
       setGCPCloudProviderCredentialId(gcpIntegrationResponse.data.cloud_provider_credentials_id)
       setIsLoading(false)
-
-      if (gcpIntegrationResponse?.data?.cloud_provider_credentials_id) {
-        setIsLoading(true);
-        var data = new PreflightCheckRequest({
-          projectId: BigInt(currentProject.id),
-          cloudProvider: EnumCloudProvider.GCP,
-          cloudProviderCredentialsId: gcpIntegrationResponse.data.cloud_provider_credentials_id
-
-        })
-        const preflightDataResp = await api.preflightCheck(
-          "<token>", data,
-          {
-            id: currentProject.id,
-          }
-        )
-        setPreflightData(preflightDataResp?.data?.Msg);
-        setIsLoading(false)
-
-      }
     }
     catch (err) {
       setIsLoading(false)
@@ -197,27 +174,13 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
             </Text>
           </AppearingDiv>
           <Spacer y={1} />
-          {isLoading ?
-            <>
-              <Placeholder>
-                <Loading />
-              </Placeholder>
-
-            </>
-            :
-
-            preFlightData ?
-              (<PreflightChecks preflightData={preFlightData} setPreflightFailed={setPreflightFailed} />)
-              : (<Text>  Could not perform preflight checks on your account. Please verify your credentials are correct or contact Porter Support at support@porter.run</Text>)
-
-          }
         </>
       </>
       )}
 
       <Spacer y={0.5} />
       <Button
-        disabled={!isContinueEnabled || preflightFailed || isLoading}
+        disabled={!isContinueEnabled || isLoading}
         onClick={saveCredentials}
       >Continue</Button>
 

+ 129 - 97
dashboard/src/components/GCPProvisionerSettings.tsx

@@ -37,6 +37,7 @@ import Placeholder from "./Placeholder";
 import Fieldset from "./porter/Fieldset";
 import ExpandableSection from "./porter/ExpandableSection";
 import PreflightChecks from "./PreflightChecks";
+import VerticalSteps from "./porter/VerticalSteps";
 
 
 const locationOptions = [
@@ -73,6 +74,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
     setCurrentCluster,
     setShouldRefreshClusters,
   } = useContext(Context);
+  const [step, setStep] = useState(0);
   const [createStatus, setCreateStatus] = useState("");
   const [clusterName, setClusterName] = useState("");
   const [region, setRegion] = useState(locationOptions[0].value);
@@ -84,8 +86,8 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
   const [errorMessage, setErrorMessage] = useState<string>("");
   const [errorDetails, setErrorDetails] = useState<string>("");
   const [isClicked, setIsClicked] = useState(false);
-  const [preflightData, setPreflightData] = useState({})
-  const [preflightFailed, setPreflightFailed] = useState<boolean>(false)
+  const [preflightData, setPreflightData] = useState(null)
+  const [preflightFailed, setPreflightFailed] = useState<boolean>(true)
   const [isLoading, setIsLoading] = useState(false);
   const [isExpanded, setIsExpanded] = useState(false);
 
@@ -367,6 +369,8 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
 
   useEffect(() => {
     if (statusPreflight() == "" && !props.clusterId) {
+      setStep(1)
+      setPreflightData(null)
       preflightChecks()
     }
 
@@ -374,8 +378,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
 
   const preflightChecks = async () => {
     setIsLoading(true);
-
-
+    setPreflightData(null);
     var data = new PreflightCheckRequest({
       projectId: BigInt(currentProject.id),
       cloudProvider: EnumCloudProvider.GCP,
@@ -398,6 +401,23 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         id: currentProject.id,
       }
     )
+    // Check if any of the preflight checks has a message
+    let hasMessage = false;
+    let errors = "Preflight Checks Failed : ";
+    for (let check in preflightDataResp?.data?.Msg.preflight_checks) {
+      if (preflightDataResp?.data?.Msg.preflight_checks[check]?.message) {
+        hasMessage = true;
+        errors = errors + check + ", "
+      }
+    }
+    // If none of the checks have a message, set setPreflightFailed to false
+    if (hasMessage) {
+      markStepStarted("provisioning-failed", errors);
+    }
+    if (!hasMessage) {
+      setPreflightFailed(false);
+      setStep(2);
+    }
     setPreflightData(preflightDataResp?.data?.Msg);
     setIsLoading(false)
 
@@ -407,100 +427,112 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
     // Render simplified form if initial create
     if (!props.clusterId) {
       return (
-        <>
-          <Text size={16}>Select a Google Cloud Region for your cluster</Text>
-          <Spacer y={1} />
-          <Text color="helper">
-            Porter will provision your infrastructure in the
-            specified location.
-          </Text>
-          <Spacer height="10px" />
+        <VerticalSteps
+          currentStep={step}
+          steps={[
+            <>
+              <Text size={16}>Select a Google Cloud Region for your cluster</Text>
+              <Spacer y={1} />
+              <Text color="helper">
+                Porter will provision your infrastructure in the
+                specified location.
+              </Text>
+              <Spacer height="10px" />
+              <SelectRow
+                options={locationOptions}
+                width="350px"
+                disabled={isReadOnly}
+                value={region}
+                scrollBuffer={true}
+                dropdownMaxHeight="240px"
+                setActiveValue={setRegion}
+                label="📍 GCP location" />
+              {renderAdvancedSettings()}
+
+            </>,
+            <>
+              <PreflightChecks provider="GCP" preflightData={preflightData} />
+              <Spacer y={.5} />
+              {(preflightFailed && preflightData) &&
+                <>
+                  <Text color="helper">
+                    Preflight checks for the account didn't pass. Please fix the issues and retry.
+                  </Text>
+                  < Button
+                    // disabled={isDisabled()}
+                    disabled={isLoading}
+                    onClick={preflightChecks}
+                  >
+                    Retry Checks
+                  </Button>
+                </>
+              }
+            </>,
+            <>
+              <Text size={16}>Provision your cluster</Text>
+              <Spacer y={1} />
+              <Button
+                disabled={isDisabled() || isLoading || preflightFailed || statusPreflight() != ""}
+                onClick={createCluster}
+                status={getStatus()}
+              >
+                Provision
+              </Button><Spacer y={1} /></>
+          ].filter((x) => x)}
+        />
+      );
+    }
+
+    // If settings, update full form
+    return (
+      <>
+        <StyledForm>
+          <Heading isAtTop>GCP configuration</Heading>
           <SelectRow
             options={locationOptions}
             width="350px"
-            disabled={isReadOnly}
+            disabled={isReadOnly || true}
             value={region}
             scrollBuffer={true}
             dropdownMaxHeight="240px"
             setActiveValue={setRegion}
-            label="📍 GCP location"
+            label="📍 Google Cloud Region"
           />
-          {renderAdvancedSettings()}
-
-        </>
-      );
-    }
-
-    // If settings, update full form
-    return (
-      <>
-        <Heading isAtTop>GCP configuration</Heading>
-        <SelectRow
-          options={locationOptions}
-          width="350px"
-          disabled={isReadOnly || true}
-          value={region}
-          scrollBuffer={true}
-          dropdownMaxHeight="240px"
-          setActiveValue={setRegion}
-          label="📍 Google Cloud Region"
-        />
-        <SelectRow
-          options={clusterVersionOptions}
-          width="350px"
-          disabled={isReadOnly}
-          value={clusterVersion}
-          scrollBuffer={true}
-          dropdownMaxHeight="240px"
-          setActiveValue={setClusterVersion}
-          label="Cluster version"
-        />
-      </>
-    );
-  };
-
-  return (
-    <>
-      <StyledForm>{renderForm()}</StyledForm>
+          <SelectRow
+            options={clusterVersionOptions}
+            width="350px"
+            disabled={isReadOnly}
+            value={clusterVersion}
+            scrollBuffer={true}
+            dropdownMaxHeight="240px"
+            setActiveValue={setClusterVersion}
+            label="Cluster version"
+          />
+        </StyledForm>
 
-      {props.credentialId && (<>
+        <Button
+          disabled={isDisabled() || isLoading || preflightFailed || statusPreflight() != ""}
+          onClick={createCluster}
+          status={getStatus()}
+        >
+          Provision
+        </Button>
 
-        {isLoading ?
+        {
+          (!currentProject?.enable_reprovision && props.clusterId) &&
           <>
-            <Placeholder>
-              <Loading />
-            </Placeholder>
             <Spacer y={1} />
-          </>
-          :
-          <>
-            {(!props.clusterId) &&
-              <>
-                <PreflightChecks preflightData={preflightData} setPreflightFailed={setPreflightFailed} />
-                <Spacer y={1} />
-              </>
-            }
+            <Text>Updates to the cluster are disabled on this project. Enable re-provisioning by contacting <a href="mailto:support@porter.run">Porter Support</a>.</Text>
           </>
         }
-
       </>
-      )}
+    );
+  };
 
-      <Button
-        disabled={isDisabled() || isLoading || preflightFailed || statusPreflight() != ""}
-        onClick={createCluster}
-        status={getStatus()}
-      >
-        Provision
-      </Button>
+  return (
+    <>
+      {renderForm()}
 
-      {
-        (!currentProject?.enable_reprovision && props.clusterId) &&
-        <>
-          <Spacer y={1} />
-          <Text>Updates to the cluster are disabled on this project. Enable re-provisioning by contacting <a href="mailto:support@porter.run">Porter Support</a>.</Text>
-        </>
-      }
 
       {user.isPorterUser &&
         <>
@@ -525,14 +557,14 @@ export default withRouter(GCPProvisionerSettings);
 
 
 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;
-      `;
+              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.";
@@ -545,14 +577,14 @@ const errorMessageToModal = (errorMessage: string) => {
 };
 
 const ExpandHeader = styled.div<{ isExpanded: boolean }>`
-  display: flex;
-  align-items: center;
-  cursor: pointer;
+              display: flex;
+              align-items: center;
+              cursor: pointer;
   > i {
-    margin-right: 7px;
-    margin-left: -7px;
-    transform: ${(props) =>
+                margin - right: 7px;
+              margin-left: -7px;
+              transform: ${(props) =>
     props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
-    transition: transform 0.1s ease;
+              transition: transform 0.1s ease;
   }
-`;
+              `;

+ 361 - 38
dashboard/src/components/PreflightChecks.tsx

@@ -2,75 +2,110 @@ import React, { useEffect, useState, useContext } from "react";
 import styled from "styled-components";
 import { RouteComponentProps, withRouter } from "react-router";
 import Spacer from "./porter/Spacer";
-
+import Step from "./porter/Step";
+import Link from "./porter/Link";
 import Text from "./porter/Text";
+import Error from "./porter/Error";
 import healthy from "assets/status-healthy.png";
 import failure from "assets/failure.svg";
-import { PREFLIGHT_MESSAGE_CONST } from "shared/util";
-
+import { PREFLIGHT_MESSAGE_CONST, PREFLIGHT_MESSAGE_CONST_AWS, PREFLIGHT_MESSAGE_CONST_GCP } from "shared/util";
+import Loading from "./Loading";
 type Props = RouteComponentProps & {
   preflightData: any
-  setPreflightFailed: (x: boolean) => void;
-};
+  provider: 'AWS' | 'GCP' | 'DEFAULT';
 
+};
 
 const PreflightChecks: React.FC<Props> = (props) => {
-  const [trackFailures, setFailures] = useState<boolean>(false)
-  const PreflightCheckItem = ({ check }) => {
-    const [isExpanded, setIsExpanded] = useState(false);
-    const hasMessage = !!check.value?.message;
-    if (hasMessage) {
-      setFailures(hasMessage)
+  const getMessageConstByProvider = (provider: 'AWS' | 'GCP' | 'DEFAULT') => {
+    switch (provider) {
+      case 'AWS':
+        return PREFLIGHT_MESSAGE_CONST_AWS;
+      case 'GCP':
+        return PREFLIGHT_MESSAGE_CONST_GCP;
+      default:
+        return PREFLIGHT_MESSAGE_CONST;
     }
+  };
+  const currentMessageConst = getMessageConstByProvider(props.provider);
+
+  const PreflightCheckItem = ({ checkKey }) => {
+    // Using optional chaining to prevent potential null/undefined errors
+    const checkData = props.preflightData?.preflight_checks?.[checkKey];
+    const hasMessage = checkData?.message;
+
+    const [isExpanded, setIsExpanded] = useState(false);
+
     const handleToggle = () => {
       if (hasMessage) {
         setIsExpanded(!isExpanded);
       }
-    }
-    props.setPreflightFailed(trackFailures)
+    };
+
+
+
     return (
-      <CheckItemContainer hasMessage={hasMessage} onClick={handleToggle}>
-        <CheckItemTop>
-          {hasMessage ? <StatusIcon src={failure} /> : <StatusIcon src={healthy} />}
+      <CheckItemContainer hasMessage={hasMessage}>
+        <CheckItemTop onClick={handleToggle}>
+          {!props.preflightData ? (
+            <Loading
+              offset="0px"
+              width="20px"
+              height="20px" />
+          ) : hasMessage ? (
+            <StatusIcon src={failure} />
+          ) : (
+            <StatusIcon src={healthy} />
+          )}
           <Spacer inline x={1} />
-          <Text style={{ marginLeft: '10px', flex: 1 }}>{PREFLIGHT_MESSAGE_CONST[check.key]}</Text>
+          <Text style={{ marginLeft: '10px', flex: 1 }}>{currentMessageConst[checkKey]}</Text>
           {hasMessage && <ExpandIcon className="material-icons" isExpanded={isExpanded}>
             arrow_drop_down
           </ExpandIcon>}
         </CheckItemTop>
         {isExpanded && hasMessage && (
           <div>
-            <ErrorMessageLabel>Error Message:</ErrorMessageLabel>
-            <ErrorMessageContent>{check.value.message}</ErrorMessageContent>
-            {check.value.metadata &&
-              Object.entries(check.value.metadata).map(([key, value]) => (
-                <div key={key}>
-                  <ErrorMessageLabel>{key}:</ErrorMessageLabel>
-                  <ErrorMessageContent>{value}</ErrorMessageContent>
-                </div>
+            <Error
+              message={checkData?.message}
+              ctaText={
+                checkData?.message !== DEFAULT_ERROR_MESSAGE
+                  ? "Troubleshooting steps"
+                  : null
+              }
+              errorModalContents={errorMessageToModal(checkData?.message)}
+            />
+            <Spacer y={.5} />
+            {checkData?.metadata &&
+              Object.entries(checkData.metadata).map(([key, value]) => (
+                <>
+                  <div key={key}>
+                    <ErrorMessageLabel>{key}:</ErrorMessageLabel>
+                    <ErrorMessageContent>{value}</ErrorMessageContent>
+                  </div>
+                </>
               ))}
           </div>
         )}
       </CheckItemContainer>
     );
   };
-
   return (
-    <div>
-      {props.preflightData && (
-        <AppearingDiv>
-          <Text> Preflight Checks </Text>
-          <Spacer y={.5} />
-          {Object.entries(props.preflightData.preflight_checks || {}).map(([key, value]) => (
-            <PreflightCheckItem key={key} check={{ key, value }} />
-          ))}
-        </AppearingDiv>
-      )}
-    </div>
+    <AppearingDiv>
+      <Text size={16}>Cluster provision check</Text>
+      <Spacer y={.5} />
+      <Text color="helper">
+        Porter checks that the account has the right permissions and resources to provision a cluster.
+      </Text>
+      <Spacer y={1} />
+      {Object.keys(currentMessageConst).map((checkKey) => (
+        <PreflightCheckItem key={checkKey} checkKey={checkKey} />
+      ))}
+    </AppearingDiv>
   );
 };
 
 
+
 export default withRouter(PreflightChecks);
 
 
@@ -136,4 +171,292 @@ const ErrorMessageContent = styled.div`
   margin-left: 10px;
   user-select: text;
   cursor: text
-`;
+`;
+
+const AWS_LOGIN_ERROR_MESSAGE =
+  "Porter could not access your AWS account. Please make sure you have granted permissions and try again.";
+const AWS_EIP_QUOTA_ERROR_MESSAGE =
+  "Your AWS account has reached the limit of elastic IPs allowed in the region. Additional addresses must be requested in order to provision.";
+const AWS_VPC_QUOTA_ERROR_MESSAGE =
+  "Your AWS account has reached the limit of VPCs allowed in the region. Additional VPCs must be requested in order to provision.";
+const AWS_NAT_GATEWAY_QUOTA_ERROR_MESSAGE =
+  "Your AWS account has reached the limit of NAT Gateways allowed in the region. Additional NAT Gateways must be requested in order to provision.";
+const AWS_VCPU_QUOTA_ERROR_MESSAGE =
+  "Your AWS account has reached the limit of vCPUs allowed in the region. Additional vCPUs must be requested in order to provision.";
+const DEFAULT_ERROR_MESSAGE =
+  "An error occurred while provisioning your infrastructure. Please try again.";
+
+const errorMessageToModal = (errorMessage: string) => {
+  switch (errorMessage) {
+    case AWS_LOGIN_ERROR_MESSAGE:
+      return (
+        <>
+          <Text size={16} weight={500}>
+            Granting Porter access to AWS
+          </Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            Porter needs access to your AWS account in order to create
+            infrastructure. You can grant Porter access to AWS 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}>Return to Porter and select "Continue".</Step>
+        </>
+      );
+    case AWS_EIP_QUOTA_ERROR_MESSAGE:
+      return (
+        <>
+          <Text size={16} weight={500}>
+            Requesting more EIP Adresses
+          </Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            You will need to either request more EIP addresses or delete
+            existing ones in order to provision in the region specified. You can
+            request more addresses by following these steps:
+          </Text>
+          <Spacer y={1} />
+          <Step number={1}>
+            Log into
+            <Spacer inline width="5px" />
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
+              your AWS account
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={2}>
+            Navigate to
+            <Spacer inline width="5px" />
+            <Link
+              to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/ec2/quotas"
+              target="_blank"
+            >
+              the Amazon Elastic Compute Cloud (Amazon EC2) Service Quotas
+              portal
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={3}>
+            Search for "EC2-VPC Elastic IPs" in the search box and click on the
+            search result.
+          </Step>
+          <Spacer y={1} />
+          <Step number={4}>
+            Click on "Request quota increase". In order to provision with
+            Porter, you will need to request at least 3 addresses above your
+            current quota limit.
+          </Step>
+          <Spacer y={1} />
+          <Step number={5}>
+            Once that request is approved, return to Porter and retry the
+            provision.
+          </Step>
+        </>
+      );
+    case AWS_VPC_QUOTA_ERROR_MESSAGE:
+      return (
+        <>
+          <Text size={16} weight={500}>
+            Requesting more VPCs
+          </Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            You will need to either request more VPCs or delete existing ones in
+            order to provision in the region specified. You can request more
+            VPCs by following these steps:
+          </Text>
+          <Spacer y={1} />
+          <Step number={1}>
+            Log into
+            <Spacer inline width="5px" />
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
+              your AWS account
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={2}>
+            Navigate to
+            <Spacer inline width="5px" />
+            <Link
+              to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/vpc/quotas"
+              target="_blank"
+            >
+              the Amazon Virtual Private Cloud (Amazon VPC) Service Quotas
+              portal
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={3}>
+            Search for "VPCs per Region" in the search box and click on the
+            search result.
+          </Step>
+          <Spacer y={1} />
+          <Step number={4}>
+            Click on "Request quota increase". In order to provision with
+            Porter, you will need to request at least 1 VPCs above your current
+            quota limit.
+          </Step>
+          <Spacer y={1} />
+          <Step number={5}>
+            Once that request is approved, return to Porter and retry the
+            provision.
+          </Step>
+        </>
+      );
+    case AWS_NAT_GATEWAY_QUOTA_ERROR_MESSAGE:
+      return (
+        <>
+          <Text size={16} weight={500}>
+            Requesting more NAT Gateways
+          </Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            You will need to either request more NAT Gateways or delete existing
+            ones in order to provision in the region specified. You can request
+            more NAT Gateways by following these steps:
+          </Text>
+          <Spacer y={1} />
+          <Step number={1}>
+            Log into
+            <Spacer inline width="5px" />
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
+              your AWS account
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={2}>
+            Navigate to
+            <Spacer inline width="5px" />
+            <Link
+              to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/vpc/quotas"
+              target="_blank"
+            >
+              the Amazon Virtual Private Cloud (Amazon VPC) Service Quotas
+              portal
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={3}>
+            Search for "NAT gateways per Availability Zone" in the search box
+            and click on the search result.
+          </Step>
+          <Spacer y={1} />
+          <Step number={4}>
+            Click on "Request quota increase". In order to provision with
+            Porter, you will need to request at least 3 NAT Gateways above your
+            current quota limit.
+          </Step>
+          <Spacer y={1} />
+          <Step number={5}>
+            Once that request is approved, return to Porter and retry the
+            provision.
+          </Step>
+        </>
+      );
+    case AWS_VCPU_QUOTA_ERROR_MESSAGE:
+      return (
+        <>
+          <Text size={16} weight={500}>
+            Requesting more vCPUs
+          </Text>
+          <Spacer y={1} />
+          <Text color="helper">
+            You will need to either request more vCPUs or delete existing
+            instances in order to provision in the region specified. You can
+            request more vCPUs by following these steps:
+          </Text>
+          <Spacer y={1} />
+          <Step number={1}>
+            Log into
+            <Spacer inline width="5px" />
+            <Link
+              to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+              target="_blank"
+            >
+              your AWS account
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={2}>
+            Navigate to
+            <Spacer inline width="5px" />
+            <Link
+              to="https://us-east-1.console.aws.amazon.com/servicequotas/home/services/ec2/quotas"
+              target="_blank"
+            >
+              the Amazon Elastic Compute Cloud (Amazon EC2) Service Quotas
+              portal
+            </Link>
+            .
+          </Step>
+          <Spacer y={1} />
+          <Step number={3}>
+            Search for "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z)
+            instances" in the search box and click on the search result.
+          </Step>
+          <Spacer y={1} />
+          <Step number={4}>
+            Click on "Request quota increase". In order to provision with
+            Porter, you will need to request at least 10 vCPUs above your
+            current quota limit.
+          </Step>
+          <Spacer y={1} />
+          <Step number={5}>
+            Once that request is approved, return to Porter and retry the
+            provision.
+          </Step>
+        </>
+      );
+    default:
+      return null;
+  }
+};

+ 0 - 8
dashboard/src/components/ProvisionerForm.tsx

@@ -39,10 +39,6 @@ const ProvisionerForm: React.FC<Props> = ({
             <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} />
         </>
       )}
@@ -58,10 +54,6 @@ const ProvisionerForm: React.FC<Props> = ({
             <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} />
         </>
       )}

+ 127 - 37
dashboard/src/components/ProvisionerSettings.tsx

@@ -21,6 +21,9 @@ import {
   LoadBalancer,
   LoadBalancerType,
   EKSLogging,
+  EKSPreflightValues,
+  PreflightCheckRequest,
+  GKE
 } from "@porter-dev/api-contracts";
 import { ClusterType } from "shared/types";
 import Button from "./porter/Button";
@@ -35,6 +38,9 @@ import Checkbox from "./porter/Checkbox";
 import Tooltip from "./porter/Tooltip";
 import Icon from "./porter/Icon";
 import Loading from "./Loading";
+import PreflightChecks from "./PreflightChecks";
+import Placeholder from "./Placeholder";
+import VerticalSteps from "./porter/VerticalSteps";
 const regionOptions = [
   { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
   { value: "us-east-2", label: "US East (Ohio) us-east-2" },
@@ -101,6 +107,7 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
   const [kmsEncryptionEnabled, setKmsEncryptionEnabled] = useState<boolean>(
     false
   );
+  const [step, setStep] = useState(0);
   const [loadBalancerType, setLoadBalancerType] = useState(false);
   const [wildCardDomain, setWildCardDomain] = useState("");
   const [IPAllowList, setIPAllowList] = useState<string>("");
@@ -124,6 +131,8 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
   const [errorMessage, setErrorMessage] = useState<string>(undefined);
   const [isClicked, setIsClicked] = useState(false);
   const [isLoading, setIsLoading] = useState(false);
+  const [preflightData, setPreflightData] = useState(null)
+  const [preflightFailed, setPreflightFailed] = useState<boolean>(true)
 
   const markStepStarted = async (step: string, errMessage?: string) => {
     try {
@@ -468,7 +477,57 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
     }
   }, [isExpanded, props.selectedClusterVersion]);
 
+  useEffect(() => {
+    if (!props.clusterId) {
+      setStep(1)
+      setPreflightData(null)
+      preflightChecks()
+    }
+  }, [props.selectedClusterVersion, awsRegion]);
+
+
+  const preflightChecks = async () => {
+    setIsLoading(true);
+    setPreflightData(null);
 
+    var data = new PreflightCheckRequest({
+      projectId: BigInt(currentProject.id),
+      cloudProvider: EnumCloudProvider.AWS,
+      cloudProviderCredentialsId: props.credentialId,
+      preflightValues: {
+        case: "eksPreflightValues",
+        value: new EKSPreflightValues({
+          region: awsRegion,
+        })
+      }
+    });
+    const preflightDataResp = await api.preflightCheck(
+      "<token>", data,
+      {
+        id: currentProject.id,
+      }
+    )
+    // Check if any of the preflight checks has a message
+    let hasMessage = false;
+    let errors = "Preflight Checks Failed : ";
+    for (let check in preflightDataResp?.data?.Msg.preflight_checks) {
+      if (preflightDataResp?.data?.Msg.preflight_checks[check]?.message) {
+        hasMessage = true;
+        errors = errors + check + ", "
+      }
+    }
+    // If none of the checks have a message, set setPreflightFailed to false
+    if (hasMessage) {
+      markStepStarted("provisioning-failed", errors);
+    }
+    if (!hasMessage) {
+      setPreflightFailed(false);
+      setStep(2);
+    }
+    setPreflightData(preflightDataResp?.data?.Msg);
+    setIsLoading(false)
+
+  }
   const renderAdvancedSettings = () => {
     return (
       <>
@@ -915,33 +974,66 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
     // Render simplified form if initial create
     if (!props.clusterId) {
       return (
-        <>
-          <Text size={16}>Select an AWS region</Text>
-          <Spacer y={1} />
-          <Text color="helper">
-            Porter will automatically provision your infrastructure in the
-            specified region.
-          </Text>
-          <Spacer height="10px" />
-          <SelectRow
-            options={regionOptions}
-            width="350px"
-            disabled={isReadOnly}
-            value={awsRegion}
-            scrollBuffer={true}
-            dropdownMaxHeight="240px"
-            setActiveValue={setAwsRegion}
-            label="📍 AWS region"
-          />
-          {(user?.isPorterUser || !currentProject?.simplified_view_enabled) &&
-            renderAdvancedSettings()}
-        </>
+        <VerticalSteps
+          currentStep={step}
+          steps={[
+            <>
+              <Text size={16}>Select an AWS region</Text><Spacer y={.5} /><Text color="helper">
+                Porter will automatically provision your infrastructure in the
+                specified region.
+              </Text><Spacer height="10px" /><SelectRow
+                options={regionOptions}
+                width="350px"
+                disabled={isReadOnly}
+                value={awsRegion}
+                scrollBuffer={true}
+                dropdownMaxHeight="240px"
+                setActiveValue={setAwsRegion}
+                label="📍 AWS region" />
+              <>
+                {
+                  user?.isPorterUser && renderAdvancedSettings()
+                }
+              </>
+            </>,
+            <>
+              <PreflightChecks provider='AWS' preflightData={preflightData} />
+              <Spacer y={.5} />
+              {(preflightFailed && preflightData) &&
+                <>
+                  <Text color="helper">
+                    Preflight checks for the account didn't pass. Please fix the issues and retry.
+                  </Text>
+                  < Button
+                    // disabled={isDisabled()}
+                    disabled={isLoading}
+                    onClick={preflightChecks}
+                  >
+                    Retry Checks
+                  </Button>
+                </>
+              }
+            </>,
+            <>
+              <Text size={16}>Provision your cluster</Text>
+              <Spacer y={1} />
+              <Button
+                // disabled={isDisabled()}
+                disabled={isDisabled() || preflightFailed || isLoading}
+                onClick={createCluster}
+                status={getStatus()}
+              >
+                Provision
+              </Button>
+              <Spacer y={1} /></>
+          ].filter((x) => x)}
+        />
       );
     }
 
     // If settings, update full form
     return (
-      <>
+      <><StyledForm>
         <Heading isAtTop>EKS configuration</Heading>
         <SelectRow
           options={regionOptions}
@@ -951,27 +1043,25 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
           scrollBuffer={true}
           dropdownMaxHeight="240px"
           setActiveValue={setAwsRegion}
-          label="📍 AWS region"
-        />
+          label="📍 AWS region" />
         {renderAdvancedSettings()}
-      </>
+      </StyledForm>
+        <Button
+          // disabled={isDisabled()}
+          disabled={isDisabled() || preflightFailed || isLoading}
+          onClick={createCluster}
+          status={getStatus()}
+        >
+          Provision
+        </Button></>
     );
   };
 
   return (
     <>
-      <StyledForm>{renderForm()}</StyledForm>
-      <Button
-        // disabled={isDisabled()}
-        disabled={
-          isDisabled()
-        }
-        onClick={createCluster}
-        status={getStatus()}
-      >
-        Provision
-      </Button>
-      {user.isPorterUser &&
+      {renderForm()}
+      {
+        user.isPorterUser &&
         <>
 
           <Spacer y={1} />

+ 17 - 1
dashboard/src/shared/util.ts

@@ -14,5 +14,21 @@ export function valueExists<T>(value: T | null | undefined): value is T {
 
 export const PREFLIGHT_MESSAGE_CONST = {
   "apiEnabled": "APIs enabled on service account",
-  "cidrAvailability": "CIDR availability"
+  "cidrAvailability": "CIDR availability",
+  "eip": "Elastic IP availability",
+  "natGateway": "NAT Gateway availability",
+  "vpc": "VPC availability",
+  "vcpus": "vCPUs availability",
+}
+
+export const PREFLIGHT_MESSAGE_CONST_AWS = {
+  "eip": "Elastic IP availability",
+  "natGateway": "NAT Gateway availability",
+  "vpc": "VPC availability",
+  "vcpus": "vCPU availability",
+}
+
+export const PREFLIGHT_MESSAGE_CONST_GCP = {
+  "apiEnabled": "APIs enabled on service account",
+  "cidrAvailability": "CIDR availability",
 }

+ 1 - 1
go.mod

@@ -81,7 +81,7 @@ require (
 	github.com/matryer/is v1.4.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.99
+	github.com/porter-dev/api-contracts v0.0.100
 	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

@@ -1512,8 +1512,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.99 h1:7VltsUOtlTPlTApmcFyAhC29QxptgS87JNoeUk7VWGk=
-github.com/porter-dev/api-contracts v0.0.99/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.0.100 h1:378cKlIjPKlTsEVeyoGTQXskxy0xFmuIpxODSD1hzmo=
+github.com/porter-dev/api-contracts v0.0.100/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=