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

Added more functionality to provisioner

jnfrati 4 лет назад
Родитель
Сommit
ba4879c80f

+ 25 - 1
dashboard/src/main/home/onboarding/components/ProviderSelector.tsx

@@ -4,13 +4,17 @@ import styled from "styled-components";
 import { SupportedProviders } from "../types";
 
 export type ProviderSelectorProps = {
-  selectProvider: (provider: SupportedProviders) => void;
+  selectProvider: (
+    provider: SupportedProviders | (SupportedProviders | "external")
+  ) => void;
+  enableExternal?: boolean;
 };
 
 const providers: SupportedProviders[] = ["aws", "gcp", "do"];
 
 const ProviderSelector: React.FC<ProviderSelectorProps> = ({
   selectProvider,
+  enableExternal,
 }) => {
   return (
     <>
@@ -36,6 +40,26 @@ const ProviderSelector: React.FC<ProviderSelectorProps> = ({
             </Block>
           );
         })}
+        {enableExternal && (
+          <Block
+            key={"external"}
+            onClick={() => {
+              selectProvider("external");
+            }}
+          >
+            <Icon src={""} />
+            <BlockTitle>External Cluster</BlockTitle>
+            <CostSection
+              onClick={(e) => {
+                e.stopPropagation();
+                selectProvider("external");
+              }}
+            ></CostSection>
+            <BlockDescription>
+              Connect your own cluster via CLI.
+            </BlockDescription>
+          </Block>
+        )}
       </BlockList>
     </>
   );

+ 0 - 1
dashboard/src/main/home/onboarding/state/index.ts

@@ -21,7 +21,6 @@ export const OFState = proxy({
       OFState.actions.saveState();
     },
     clearState: () => {
-      console.log("CLEARED STATE");
       StateHandler.actions.clearState();
       StepHandler.actions.clearState();
       ConnectRegistryState.actions.clearState();

+ 3 - 1
dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistry.tsx

@@ -58,7 +58,9 @@ const ConnectRegistry = () => {
         <>
           <ProviderSelector
             selectProvider={(provider) => {
-              State.selectedProvider = provider;
+              if (provider !== "external") {
+                State.selectedProvider = provider;
+              }
             }}
           />
           <NextStep

+ 16 - 12
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx

@@ -8,13 +8,14 @@ import styled from "styled-components";
 import { useSnapshot } from "valtio";
 import ProviderSelector from "../../components/ProviderSelector";
 import { OFState } from "../../state";
-import { SupportedProviders } from "../../types";
 
 import { State } from "./ProvisionResourcesState";
 import FormFlowWrapper from "./forms/FormFlow";
+import ConnectExternalCluster from "./forms/_ConnectExternalCluster";
 
 const ProvisionResources = () => {
   const snap = useSnapshot(State);
+  const globalFormSnap = useSnapshot(OFState);
   const { getQueryParam } = useRouting();
   const location = useLocation();
 
@@ -25,6 +26,11 @@ const ProvisionResources = () => {
     }
   }, [location]);
 
+  useEffect(() => {
+    const connectedRegistry = globalFormSnap.StateHandler.connected_registry;
+    State.shouldProvisionRegistry = !!connectedRegistry?.skip;
+  }, [globalFormSnap.StateHandler.connected_registry]);
+
   const nextStep = (skipped: boolean) => {
     if (skipped) {
       OFState.actions.nextStep({
@@ -49,23 +55,21 @@ const ProvisionResources = () => {
         applications.
       </Helper>
       {snap.selectedProvider ? (
-        <FormFlowWrapper nextStep={() => nextStep(false)} />
+        snap.selectedProvider !== "external" ? (
+          <FormFlowWrapper nextStep={() => nextStep(false)} />
+        ) : (
+          <ConnectExternalCluster
+            nextStep={() => nextStep(true)}
+            project={globalFormSnap.StateHandler.project}
+          />
+        )
       ) : (
         <>
           <ProviderSelector
             selectProvider={(provider) => {
               State.selectedProvider = provider;
             }}
-          />
-          <NextStep
-            text="Skip step"
-            disabled={false}
-            onClick={() => nextStep(true)}
-            status={""}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="right"
-            saveText=""
+            enableExternal={true}
           />
         </>
       )}

+ 5 - 2
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResourcesState.ts

@@ -2,11 +2,13 @@ import { proxy } from "valtio";
 import { ProvisionerConfig } from "../../state/StateHandler";
 import { SkipProvisionConfig, SupportedProviders } from "../../types";
 
-type AllowedSteps = "credentials" | "settings" | "status" | null;
+type AllowedSteps = "credentials" | "settings" | null;
 
 interface ConnectRegistryState {
-  selectedProvider: SupportedProviders | null;
+  selectedProvider: SupportedProviders | "external" | null;
+  shouldProvisionRegistry: boolean;
   currentStep: AllowedSteps;
+
   config: Partial<Exclude<ProvisionerConfig, SkipProvisionConfig>> | null;
   actions: {
     selectProvider: (provider: SupportedProviders) => void;
@@ -17,6 +19,7 @@ interface ConnectRegistryState {
 
 const initialState: ConnectRegistryState = {
   selectedProvider: null,
+  shouldProvisionRegistry: false,
   currentStep: "credentials",
   config: null,
   actions: {

+ 18 - 20
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/FormFlow.tsx

@@ -27,17 +27,17 @@ const Forms = {
   aws: {
     credentials: AWSCredentialsForm,
     settings: AWSSettingsForm,
-    status: AWSProvisionerStatus,
+    // status: AWSProvisionerStatus,
   },
   gcp: {
     credentials: GCPCredentialsForm,
     settings: GCPSettingsForm,
-    status: GCPProvisionerStatus,
+    // status: GCPProvisionerStatus,
   },
   do: {
     credentials: DOCredentialsForm,
     settings: DOSettingsForm,
-    status: DOProvisionerStatus,
+    // status: DOProvisionerStatus,
   },
 };
 
@@ -63,38 +63,36 @@ const FormFlowWrapper: React.FC<Props> = ({ nextStep }) => {
       State.currentStep = "settings";
     } else if (snap.currentStep === "settings") {
       State.config.settings = data.settings;
-      State.currentStep = "status";
-    } else if (snap.currentStep === "status") {
       nextStep();
     }
   };
 
   const CurrentForm = useMemo(() => {
-    const providerSteps = Forms[snap.selectedProvider];
-    if (!providerSteps) {
-      return null;
-    }
+    if (snap.selectedProvider !== "external") {
+      const providerSteps = Forms[snap.selectedProvider];
+      if (!providerSteps) {
+        return null;
+      }
 
-    const currentForm = providerSteps[snap.currentStep];
-    if (!currentForm) {
-      return null;
-    }
+      const currentForm = providerSteps[snap.currentStep];
+      if (!currentForm) {
+        return null;
+      }
 
-    return React.createElement(currentForm as any, {
-      nextFormStep,
-      project: currentProject,
-    });
+      return React.createElement(currentForm as any, {
+        nextFormStep,
+        project: currentProject,
+      });
+    }
   }, [snap.currentStep, snap.selectedProvider]);
 
   return (
     <>
-      {FormTitle[snap.selectedProvider]}
+      {snap.selectedProvider !== "external" && FormTitle[snap.selectedProvider]}
       <Breadcrumb>
         <Text bold={snap.currentStep === "credentials"}>Credentials</Text>
         {" > "}
         <Text bold={snap.currentStep === "settings"}>Settings</Text>
-        {" > "}
-        <Text bold={snap.currentStep === "status"}>Status</Text>
       </Breadcrumb>
       {CurrentForm}
     </>

+ 9 - 0
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvsionerForm.tsx

@@ -209,6 +209,12 @@ export const SettingsForm: React.FC<{
       setButtonStatus(validation.error);
       return;
     }
+    const integrationId = `${snap.config.credentials.id}`;
+
+    if (snap.shouldProvisionRegistry) {
+      await provisionECR(integrationId);
+    }
+    await provisionEKS(integrationId);
 
     nextFormStep({
       settings: {
@@ -253,6 +259,9 @@ export const SettingsForm: React.FC<{
   );
 };
 
+/**
+ * @todo Need to implement provisioner status here
+ */
 export const Status: React.FC<{ nextFormStep: () => void }> = ({
   nextFormStep,
 }) => {

+ 248 - 0
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_ConnectExternalCluster.tsx

@@ -0,0 +1,248 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import TabSelector from "components/TabSelector";
+
+type Props = {
+  nextStep: () => void;
+  project: {
+    id: number;
+    name: string;
+  };
+};
+
+const tabOptions = [{ label: "MacOS", value: "mac" }];
+
+/**
+ * @todo Poll the available clusters until there's at least one connected
+ * to the project
+ */
+const ConnectExternalCluster: React.FC<Props> = ({ nextStep, project }) => {
+  const [currentPage, setCurrentPage] = useState(0);
+  const [currentTab, setCurrentTab] = useState("mac");
+
+  const renderPage = () => {
+    switch (currentPage) {
+      case 0:
+        return (
+          <Placeholder>
+            1. To install the Porter CLI, first retrieve the latest binary:
+            <Code>
+              &#123;
+              <br />
+              name=$(curl -s
+              https://api.github.com/repos/porter-dev/porter/releases/latest |
+              grep "browser_download_url.*/porter_.*_Darwin_x86_64\.zip" | cut
+              -d ":" -f 2,3 | tr -d \")
+              <br />
+              name=$(basename $name)
+              <br />
+              curl -L
+              https://github.com/porter-dev/porter/releases/latest/download/$name
+              --output $name
+              <br />
+              unzip -a $name
+              <br />
+              rm $name
+              <br />
+              &#125;
+            </Code>
+            2. Move the file into your bin:
+            <Code>
+              chmod +x ./porter
+              <br />
+              sudo mv ./porter /usr/local/bin/porter
+            </Code>
+            3. Log in to the Porter CLI:
+            <Code>
+              porter config set-host {location.protocol + "//" + location.host}
+              <br />
+              porter auth login
+            </Code>
+            4. Configure the Porter CLI and link your current context:
+            <Code>
+              porter config set-project {project.id}
+              <br />
+              porter connect kubeconfig
+            </Code>
+          </Placeholder>
+        );
+      case 1:
+        return (
+          <Placeholder>
+            <Bold>Passing a kubeconfig explicitly</Bold>
+            You can pass a path to a kubeconfig file explicitly via:
+            <Code>
+              porter connect kubeconfig --kubeconfig path/to/kubeconfig
+            </Code>
+            <Bold>Passing a context list</Bold>
+            You can initialize Porter with a set of contexts by passing a
+            context list to start. The contexts that Porter will be able to
+            access are the same as kubectl config get-contexts. For example, if
+            there are two contexts named minikube and staging, you could connect
+            both of them via:
+            <Code>
+              porter connect kubeconfig --context minikube --context staging
+            </Code>
+          </Placeholder>
+        );
+      default:
+        return;
+    }
+  };
+
+  return (
+    <StyledClusterInstructionsModal>
+      <TabSelector
+        options={tabOptions}
+        currentTab={currentTab}
+        setCurrentTab={(value: string) => setCurrentTab(value)}
+      />
+
+      {renderPage()}
+      <PageSection>
+        <PageCount>{currentPage + 1}/2</PageCount>
+        <i
+          className="material-icons"
+          onClick={() =>
+            currentPage > 0 ? setCurrentPage(currentPage - 1) : null
+          }
+        >
+          arrow_back
+        </i>
+        <i
+          className="material-icons"
+          onClick={() =>
+            currentPage < 1 ? setCurrentPage(currentPage + 1) : null
+          }
+        >
+          arrow_forward
+        </i>
+      </PageSection>
+    </StyledClusterInstructionsModal>
+  );
+};
+
+export default ConnectExternalCluster;
+
+const PageCount = styled.div`
+  margin-right: 9px;
+  user-select: none;
+  letter-spacing: 2px;
+`;
+
+const PageSection = styled.div`
+  position: absolute;
+  bottom: 22px;
+  right: 20px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  color: #ffffff;
+  justify-content: flex-end;
+  user-select: none;
+
+  > i {
+    font-size: 18px;
+    margin-left: 2px;
+    cursor: pointer;
+    border-radius: 20px;
+    padding: 5px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
+const Code = styled.div`
+  background: #181b21;
+  padding: 10px 15px;
+  border: 1px solid #ffffff44;
+  border-radius: 5px;
+  margin: 10px 0px 15px;
+  color: #ffffff;
+  font-size: 13px;
+  user-select: text;
+  line-height: 1em;
+  font-family: monospace;
+`;
+
+const A = styled.a`
+  color: #ffffff;
+  text-decoration: underline;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+`;
+
+const Placeholder = styled.div`
+  color: #aaaabb;
+  font-size: 13px;
+  margin-left: 0px;
+  margin-top: 25px;
+  line-height: 1.6em;
+  user-select: none;
+`;
+
+const Bold = styled.div`
+  font-weight: 600;
+  margin-bottom: 7px;
+`;
+
+const Subtitle = styled.div`
+  padding: 17px 0px 25px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  margin-top: 3px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const ModalTitle = styled.div`
+  margin: 0px 0px 13px;
+  display: flex;
+  flex: 1;
+  font-family: Work Sans, sans-serif;
+  font-size: 18px;
+  color: #ffffff;
+  user-select: none;
+  font-weight: 700;
+  align-items: center;
+  position: relative;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const CloseButton = styled.div`
+  position: absolute;
+  display: block;
+  width: 40px;
+  height: 40px;
+  padding: 13px 0 12px 0;
+  z-index: 1;
+  text-align: center;
+  border-radius: 50%;
+  right: 15px;
+  top: 12px;
+  cursor: pointer;
+  :hover {
+    background-color: #ffffff11;
+  }
+`;
+
+const CloseButtonImg = styled.img`
+  width: 14px;
+  margin: 0 auto;
+`;
+
+const StyledClusterInstructionsModal = styled.div`
+  width: 100%;
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100%;
+  padding: 25px 32px;
+  overflow: hidden;
+  border-radius: 6px;
+  background: #202227;
+`;

+ 101 - 31
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_DOProvisionerForm.tsx

@@ -2,8 +2,8 @@ import Helper from "components/form-components/Helper";
 import InputRow from "components/form-components/InputRow";
 import SelectRow from "components/form-components/SelectRow";
 import SaveButton from "components/SaveButton";
-import { DORegistryConfig } from "main/home/onboarding/types";
-import React, { useContext, useEffect, useState } from "react";
+import { DOProvisionerConfig } from "main/home/onboarding/types";
+import React, { useEffect, useState } from "react";
 import api from "shared/api";
 import styled from "styled-components";
 import { useSnapshot } from "valtio";
@@ -28,7 +28,7 @@ const regionOptions = [
 ];
 
 /**
- * This will redirect to DO, and we should pass the redirection URI to be /onboarding/registry?provider=do
+ * This will redirect to DO, and we should pass the redirection URI to be /onboarding/provision?provider=do
  *
  * After the oauth flow comes back, the first render will go and check if it exists a integration_id for DO in the
  * current onboarding project, after getting it, the CredentialsForm will use nextFormStep to save the onboarding state.
@@ -36,7 +36,7 @@ const regionOptions = [
  * If it happens to be an error, it will be shown with the default error handling through the modal.
  */
 export const CredentialsForm: React.FC<{
-  nextFormStep: (data: Partial<DORegistryConfig>) => void;
+  nextFormStep: (data: Partial<DOProvisionerConfig>) => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
   useEffect(() => {
@@ -67,53 +67,120 @@ export const CredentialsForm: React.FC<{
 };
 
 export const SettingsForm: React.FC<{
-  nextFormStep: (data: Partial<DORegistryConfig>) => void;
+  nextFormStep: (data: Partial<DOProvisionerConfig>) => void;
   project: any;
 }> = ({ nextFormStep, project }) => {
-  const [registryUrl, setRegistryUrl] = useState("basic");
-  const [registryName, setRegistryName] = useState("");
-  const [buttonStatus] = useState("");
   const snap = useSnapshot(State);
+  const [buttonStatus, setButtonStatus] = useState("");
+  const [tier, setTier] = useState("basic");
+  const [region, setRegion] = useState("nyc1");
+  const [clusterName, setClusterName] = useState("");
 
-  const submit = async () => {
-    await api.connectDORegistry(
+  const validate = () => {
+    if (!clusterName) {
+      return {
+        hasError: true,
+        error: "Cluster name cannot be empty",
+      };
+    }
+    if (clusterName.length > 25) {
+      return {
+        hasError: true,
+        error: "Cluster name cannot be longer than 25 characters",
+      };
+    }
+    return {
+      hasError: false,
+      error: "",
+    };
+  };
+
+  const provisionDOCR = async (integrationId: number, tier: string) => {
+    console.log("Provisioning DOCR...");
+    await api.createDOCR(
       "<token>",
       {
-        name: registryName,
-        do_integration_id: snap.config.credentials.id,
-        url: registryUrl,
+        do_integration_id: integrationId,
+        docr_name: project.name,
+        docr_subscription_tier: tier,
       },
-      { project_id: project.id }
+      {
+        project_id: project.id,
+      }
+    );
+  };
+
+  const provisionDOKS = async (
+    integrationId: number,
+    region: string,
+    clusterName: string
+  ) => {
+    console.log("Provisioning DOKS...");
+    await api.createDOKS(
+      "<token>",
+      {
+        do_integration_id: integrationId,
+        doks_name: clusterName,
+        do_region: region,
+      },
+      {
+        project_id: project.id,
+      }
     );
+  };
+
+  const submit = async () => {
+    const validation = validate();
+
+    if (validation.hasError) {
+      setButtonStatus(validation.error);
+      return;
+    }
+    const integrationId = snap.config.credentials.id;
+
+    if (snap.shouldProvisionRegistry) {
+      await provisionDOCR(integrationId, tier);
+    }
+    await provisionDOKS(integrationId, region, clusterName);
+
     nextFormStep({
       settings: {
-        registry_url: registryUrl,
+        region,
+        tier,
+        cluster_name: clusterName,
       },
     });
   };
 
   return (
     <>
-      <InputRow
-        type="text"
-        value={registryName}
-        setValue={(registryName: string) => setRegistryName(registryName)}
-        isRequired={true}
-        label="🏷️ Registry Name"
-        placeholder="ex: paper-straw"
+      <SelectRow
+        options={tierOptions}
+        width="100%"
+        value={tier}
+        setActiveValue={(x: string) => {
+          setTier(x);
+        }}
+        label="💰 Subscription Tier"
+      />
+      <SelectRow
+        options={regionOptions}
         width="100%"
+        dropdownMaxHeight="240px"
+        value={region}
+        setActiveValue={(x: string) => {
+          setRegion(x);
+        }}
+        label="📍 DigitalOcean Region"
       />
-      <Helper>
-        DOC R URI, in the form{" "}
-        <CodeBlock>registry.digitalocean.com/[REGISTRY_NAME]</CodeBlock>. For
-        example, <CodeBlock>registry.digitalocean.com/porter-test</CodeBlock>.
-      </Helper>
       <InputRow
         type="text"
-        value={registryUrl}
-        setValue={(url: string) => setRegistryUrl(url)}
-        label="🔗 GCR URL"
-        placeholder="ex: registry.digitalocean.com/porter-test"
+        value={clusterName}
+        setValue={(x: string) => {
+          setClusterName(x);
+        }}
+        label="Cluster Name"
+        placeholder="ex: porter-cluster"
         width="100%"
         isRequired={true}
       />
@@ -130,6 +197,9 @@ export const SettingsForm: React.FC<{
   );
 };
 
+/**
+ * @todo Need to implement provisioner status here
+ */
 export const Status: React.FC<{
   nextFormStep: () => void;
   project: any;

+ 6 - 1
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx

@@ -170,7 +170,9 @@ export const SettingsForm: React.FC<{
 
     setButtonStatus("loading");
 
-    await provisionGCR(integrationId);
+    if (snap.shouldProvisionRegistry) {
+      await provisionGCR(integrationId);
+    }
     await provisionGKE(integrationId);
     nextFormStep({
       settings: {
@@ -234,6 +236,9 @@ export const SettingsForm: React.FC<{
   );
 };
 
+/**
+ * @todo Need to implement provisioner status here
+ */
 export const Status: React.FC<{
   nextFormStep: () => void;
   project: any;