Procházet zdrojové kódy

'feature flag' new credential flow + preflight checks

Justin Rhee před 3 roky
rodič
revize
672c83985d

+ 8 - 1
dashboard/src/components/CredentialsForm.tsx

@@ -22,6 +22,7 @@ import Spacer from "./porter/Spacer";
 type Props = {
   goBack: () => void;
   proceed: (x: any) => void;
+  enableAssumeRole?: () => void;
 };
 
 type AWSCredential = {
@@ -36,6 +37,7 @@ type AWSCredential = {
 const CredentialsForm: React.FC<Props> = ({
   goBack,
   proceed,
+  enableAssumeRole,
 }) => {
   const { currentProject } = useContext(Context);
   const [awsCredentials, setAWSCredentials] = useState<AWSCredential[]>(null);
@@ -161,7 +163,12 @@ const CredentialsForm: React.FC<Props> = ({
           <InputRow
             type="password"
             value={awsSecretAccessKey}
-            setValue={(e: string) => setAWSSecretAccessKey(e)}
+            setValue={(e: string) => {
+              if (e === "open-sesame") {
+                enableAssumeRole();
+              }
+              setAWSSecretAccessKey(e)
+            }}
             label="🔒 AWS secret key"
             placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
             isRequired

+ 14 - 1
dashboard/src/components/ProvisionerFlow.tsx

@@ -31,6 +31,7 @@ const ProvisionerFlow: React.FC<Props> = ({
   const [showCostConfirmModal, setShowCostConfirmModal] = useState(false);
   const [confirmCost, setConfirmCost] = useState("");
   const [AWSAccountID, setAWSAccountID] = useState("");
+  const [useAssumeRole, setUseAssumeRole] = useState(false);
 
   const isUsageExceeded = useMemo(() => {
     if (!hasBillingEnabled) {
@@ -153,7 +154,7 @@ const ProvisionerFlow: React.FC<Props> = ({
         )}
       </>
     );
-  } else if (currentStep === "credentials") {
+  } else if (currentStep === "credentials" && useAssumeRole) {
     return (
       <CloudFormationForm
         goBack={() => setCurrentStep("cloud")}
@@ -165,12 +166,24 @@ const ProvisionerFlow: React.FC<Props> = ({
         setAWSAccountID={setAWSAccountID}
       />
     );
+  } else if (currentStep === "credentials" && !useAssumeRole) {
+    return (
+      <CredentialsForm
+        enableAssumeRole={() => setUseAssumeRole(true)}
+        goBack={() => setCurrentStep("cloud")}
+        proceed={(id) => {
+          setCredentialId(id);
+          setCurrentStep("cluster");
+        }}
+      />
+    );
   } else if (currentStep === "cluster") {
     return (
       <ProvisionerForm
         goBack={() => setCurrentStep("credentials")}
         credentialId={credentialId}
         AWSAccountID={AWSAccountID}
+        useAssumeRole={useAssumeRole}
       />
     );
   }

+ 9 - 2
dashboard/src/components/ProvisionerForm.tsx

@@ -6,17 +6,20 @@ import aws from "assets/aws.png";
 import Heading from "components/form-components/Heading";
 import Helper from "./form-components/Helper";
 import ProvisionerSettings from "./ProvisionerSettings";
+import ProvisionerSettingsOld from "./ProvisionerSettingsOld";
 
 type Props = {
   goBack: () => void;
   credentialId: string;
   AWSAccountID: string;
+  useAssumeRole?: boolean;
 };
 
 const ProvisionerForm: React.FC<Props> = ({
   goBack,
   credentialId,
-  AWSAccountID
+  AWSAccountID,
+  useAssumeRole,
 }) => {
   return (
     <>
@@ -32,7 +35,11 @@ const ProvisionerForm: React.FC<Props> = ({
       <Helper>
         Configure settings for your new cluster.
       </Helper>
-      <ProvisionerSettings credentialId={credentialId} AWSAccountID={AWSAccountID} />
+      {useAssumeRole ? (
+        <ProvisionerSettings credentialId={credentialId} AWSAccountID={AWSAccountID} />
+      ) : (
+        <ProvisionerSettingsOld credentialId={credentialId} />
+      )}
     </>
   );
 };

+ 340 - 0
dashboard/src/components/ProvisionerSettingsOld.tsx

@@ -0,0 +1,340 @@
+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 SaveButton from "./SaveButton";
+import { Contract, EnumKubernetesKind, EnumCloudProvider, NodeGroupType, EKSNodeGroup, EKS, Cluster } from "@porter-dev/api-contracts";
+import { ClusterType } from "shared/types";
+
+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" },
+  { value: "us-west-1", label: "US West (N. California) us-west-1" },
+  { value: "us-west-2", label: "US West (Oregon) us-west-2" },
+  { value: "af-south-1", label: "Africa (Cape Town) af-south-1" },
+  { value: "ap-east-1", label: "Asia Pacific (Hong Kong) ap-east-1" },
+  { value: "ap-south-1", label: "Asia Pacific (Mumbai) ap-south-1" },
+  { value: "ap-northeast-2", label: "Asia Pacific (Seoul) ap-northeast-2" },
+  { value: "ap-southeast-1", label: "Asia Pacific (Singapore) ap-southeast-1" },
+  { value: "ap-southeast-2", label: "Asia Pacific (Sydney) ap-southeast-2" },
+  { value: "ap-northeast-1", label: "Asia Pacific (Tokyo) ap-northeast-1" },
+  { value: "ca-central-1", label: "Canada (Central) ca-central-1" },
+  { value: "eu-central-1", label: "Europe (Frankfurt) eu-central-1" },
+  { value: "eu-west-1", label: "Europe (Ireland) eu-west-1" },
+  { value: "eu-west-2", label: "Europe (London) eu-west-2" },
+  { value: "eu-south-1", label: "Europe (Milan) eu-south-1" },
+  { value: "eu-west-3", label: "Europe (Paris) eu-west-3" },
+  { value: "eu-north-1", label: "Europe (Stockholm) eu-north-1" },
+  { value: "me-south-1", label: "Middle East (Bahrain) me-south-1" },
+  { value: "sa-east-1", label: "South America (São Paulo) sa-east-1" },
+];
+
+const machineTypeOptions = [
+  { value: "t3.medium", label: "t3.medium" },
+  { value: "t3.large", label: "t3.large" },
+  { value: "t3.xlarge", label: "t3.xlarge" },
+  { value: "t3.2xlarge", label: "t3.2xlarge" },
+];
+
+const clusterVersionOptions = [
+  { value: "v1.24.0", label: "1.24.0" },
+  { value: "v1.25.0", label: "1.25.0" },
+];
+
+type Props = RouteComponentProps & {
+  selectedClusterVersion?: Contract;
+  credentialId: string;
+  clusterId?: number;
+};
+
+const ProvisionerSettingsOld: React.FC<Props> = props => {
+  const {
+    user,
+    currentProject,
+    currentCluster,
+    setCurrentCluster,
+    setShouldRefreshClusters,
+    setHasFinishedOnboarding,
+  } = useContext(Context);
+  const [createStatus, setCreateStatus] = useState("");
+  const [clusterName, setClusterName] = useState("");
+  const [awsRegion, setAwsRegion] = useState("us-east-1");
+  const [machineType, setMachineType] = useState("t3.xlarge");
+  const [isExpanded, setIsExpanded] = useState(false);
+  const [minInstances, setMinInstances] = useState(1);
+  const [maxInstances, setMaxInstances] = useState(10);
+  const [cidrRange, setCidrRange] = useState("172.0.0.0/16");
+  const [clusterVersion, setClusterVersion] = useState("v1.24.0");
+  const [isReadOnly, setIsReadOnly] = useState(false);
+
+  const markProvisioningStarted = async () => {
+    try {
+      const res = await api.updateOnboardingStep(
+        "<token>", 
+        { step: "provisioning-started" }, 
+        {}
+      );
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
+  const createCluster = async () => {
+    markProvisioningStarted();
+    
+    var data = new Contract({
+      cluster: new Cluster({
+        projectId: currentProject.id,
+        kind: EnumKubernetesKind.EKS,
+        cloudProvider: EnumCloudProvider.AWS,
+        cloudProviderCredentialsId: String(props.credentialId),
+        kindValues: {
+          case: "eksKind",
+          value: new EKS({
+            clusterName,
+            clusterVersion: clusterVersion || "v1.24.0",
+            cidrRange: cidrRange || "172.0.0.0/16",
+            region: awsRegion,
+            nodeGroups: [
+              new EKSNodeGroup({
+                instanceType: "t3.medium",
+                minInstances: 1,
+                maxInstances: 5,
+                nodeGroupType: NodeGroupType.SYSTEM,
+                isStateful: false,
+              }),
+              new EKSNodeGroup({
+                instanceType: "t3.large",
+                minInstances: 1,
+                maxInstances: 5,
+                nodeGroupType: NodeGroupType.MONITORING,
+                isStateful: false,
+              }),
+              new EKSNodeGroup({
+                instanceType: machineType,
+                minInstances: minInstances || 1,
+                maxInstances: maxInstances || 10,
+                nodeGroupType: NodeGroupType.APPLICATION,
+                isStateful: false,
+              })
+            ]
+          })
+        },
+      })
+    });
+
+    if (props.clusterId) {
+      data["cluster"]["clusterId"] = props.clusterId;
+    }
+
+    try {
+      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);
+          });
+      }
+    } catch (err) {
+      console.log(err);
+    }
+  }
+
+  useEffect(() => {
+    setIsReadOnly(
+      props.clusterId && (
+        currentCluster.status === "UPDATING" ||
+        currentCluster.status === "UPDATING_UNAVAILABLE"
+      )
+    );
+    setClusterName(`${currentProject.name}-cluster`);
+  }, []);
+
+  useEffect(() => {
+    const contract = props.selectedClusterVersion as any;
+    if (contract?.cluster) {
+      contract.cluster.eksKind.nodeGroups.map((nodeGroup: any) => {
+        if (nodeGroup.nodeGroupType === "NODE_GROUP_TYPE_APPLICATION") {
+          setMachineType(nodeGroup.instanceType);
+          setMinInstances(nodeGroup.minInstances);
+          setMaxInstances(nodeGroup.maxInstances);
+        }
+      });
+      setCreateStatus("");
+      setClusterName(contract.cluster.eksKind.clusterName);
+      setAwsRegion(contract.cluster.eksKind.region);
+      setClusterVersion(contract.cluster.eksKind.clusterVersion);
+      setCidrRange(contract.cluster.eksKind.cidrRange);
+    }
+  }, [props.selectedClusterVersion]);
+
+  const renderForm = () => {
+    
+    // Render simplified form if initial create
+    if (!props.clusterId) {
+      return (
+        <>
+          <Heading isAtTop>Select an AWS region</Heading>
+          <Helper>
+            Porter will automatically provision your infrastructure in the specified region.
+          </Helper>
+          <SelectRow
+            options={regionOptions}
+            width="350px"
+            disabled={isReadOnly}
+            value={awsRegion}
+            scrollBuffer={true}
+            dropdownMaxHeight="240px"
+            setActiveValue={setAwsRegion}
+            label="📍 AWS region"
+          />
+        </>
+      )
+    }
+
+    // If settings, update full form
+    return (
+      <>
+        <Heading isAtTop>EKS configuration</Heading>
+        <SelectRow
+          options={regionOptions}
+          width="350px"
+          disabled={isReadOnly || true}
+          value={awsRegion}
+          scrollBuffer={true}
+          dropdownMaxHeight="240px"
+          setActiveValue={setAwsRegion}
+          label="📍 AWS region"
+        />
+        {
+          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: 172.0.0.0/16"
+              />
+            </>
+          )
+        }
+      </>
+    )
+  }
+
+  return (
+    <>
+      <StyledForm>
+        {renderForm()}
+      </StyledForm>
+      <SaveButton
+        disabled={(!clusterName && true) || isReadOnly}
+        onClick={createCluster}
+        clearPosition
+        text="Provision"
+        statusPosition="right"
+        status={isReadOnly && "Provisioning is still in progress"}
+      />
+    </>
+  );
+};
+
+export default withRouter(ProvisionerSettingsOld);
+
+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: #26292e;
+  border: 1px solid #494b4f;
+  font-size: 13px;
+  margin-bottom: 30px;
+`;