Răsfoiți Sursa

egress IP GCP

Stefan McShane 2 ani în urmă
părinte
comite
d9e836ad7f

+ 99 - 0
cli/cmd/v2/app_events.go

@@ -0,0 +1,99 @@
+package v2
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+func createBuildEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint) (string, error) {
+	ctx, span := telemetry.NewSpan(ctx, "create-build-event")
+	defer span.End()
+
+	req := &types.CreateOrUpdatePorterAppEventRequest{
+		Status:             types.PorterAppEventStatus_Progressing,
+		Type:               types.PorterAppEventType_Build,
+		TypeExternalSource: "GITHUB",
+		Metadata:           make(map[string]interface{}),
+	}
+
+	actionRunID := os.Getenv("GITHUB_RUN_ID")
+	if actionRunID != "" {
+		arid, err := strconv.Atoi(actionRunID)
+		if err != nil {
+			fmt.Println("could not parse action run id")
+			return "", telemetry.Error(ctx, span, err, "could not parse action run id")
+		}
+		req.Metadata["action_run_id"] = arid
+
+		repoName := os.Getenv("GITHUB_REPOSITORY")
+		parsedRepoName := strings.Split(repoName, "/")
+		if len(parsedRepoName) != 2 {
+			fmt.Println("repo name is not in the format owner/name")
+			return "", telemetry.Error(ctx, span, nil, "repo name is not in the format owner/name")
+		}
+		req.Metadata["repo"] = parsedRepoName[1]
+
+		repoOwnerAccountID := os.Getenv("GITHUB_REPOSITORY_OWNER_ID")
+		if repoOwnerAccountID != "" {
+			arid, err := strconv.Atoi(repoOwnerAccountID)
+			if err != nil {
+				fmt.Println("could not parse repo owner account id")
+				return "", telemetry.Error(ctx, span, err, "could not parse repo owner account id")
+			}
+			req.Metadata["github_account_id"] = arid
+		}
+	}
+
+	event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req)
+	if err != nil {
+		fmt.Println("could not create build event")
+		return "", telemetry.Error(ctx, span, err, "could not create build event")
+	}
+
+	return event.ID, nil
+}
+
+func createPredeployEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint, createdAt time.Time) (string, error) {
+	ctx, span := telemetry.NewSpan(ctx, "create-predeploy-event")
+	defer span.End()
+
+	req := &types.CreateOrUpdatePorterAppEventRequest{
+		Status:   types.PorterAppEventStatus_Progressing,
+		Type:     types.PorterAppEventType_PreDeploy,
+		Metadata: make(map[string]interface{}),
+	}
+	req.Metadata["start_time"] = createdAt
+
+	event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req)
+	if err != nil {
+		return "", telemetry.Error(ctx, span, err, "could not create predeploy event")
+	}
+
+	return event.ID, nil
+}
+
+func updateExistingEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint, eventID string, status types.PorterAppEventStatus, metadata map[string]interface{}) error {
+	ctx, span := telemetry.NewSpan(ctx, "update-existing-event")
+	defer span.End()
+
+	req := &types.CreateOrUpdatePorterAppEventRequest{
+		ID:       eventID,
+		Status:   status,
+		Metadata: metadata,
+	}
+
+	_, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req)
+	if err != nil {
+		return telemetry.Error(ctx, span, err, "could not update existing app event")
+	}
+
+	return nil
+}

+ 18 - 1
cli/cmd/v2/apply.go

@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/porter-dev/porter/api/server/handlers/porter_app"
+	"github.com/porter-dev/porter/api/types"
 
 	"github.com/cli/cli/git"
 
@@ -105,6 +106,8 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por
 	if applyResp.CLIAction == porterv1.EnumCLIAction_ENUM_CLI_ACTION_BUILD {
 		color.New(color.FgGreen).Printf("Building new image...\n") // nolint:errcheck,gosec
 
+		eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster)
+
 		if commitSHA == "" {
 			return errors.New("Build is required but commit SHA cannot be identified. Please set the PORTER_COMMIT_SHA environment variable or run apply in git repository with access to the git CLI.")
 		}
@@ -138,11 +141,14 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por
 
 		err = build(ctx, client, buildSettings)
 		if err != nil {
+			_ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, eventID, types.PorterAppEventStatus_Failed, nil)
 			return fmt.Errorf("error building app: %w", err)
 		}
 
 		color.New(color.FgGreen).Printf("Successfully built image (tag: %s)\n", buildSettings.ImageTag) // nolint:errcheck,gosec
 
+		_ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, eventID, types.PorterAppEventStatus_Success, nil)
+
 		applyResp, err = client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, "", "", applyResp.AppRevisionId)
 		if err != nil {
 			return fmt.Errorf("apply error post-build: %w", err)
@@ -153,6 +159,9 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por
 		color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec
 
 		now := time.Now().UTC()
+		eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, now)
+
+		eventStatus := types.PorterAppEventStatus_Success
 		for {
 			if time.Since(now) > checkPredeployTimeout {
 				return errors.New("timed out waiting for predeploy to complete")
@@ -163,13 +172,21 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por
 				return fmt.Errorf("error calling predeploy status endpoint: %w", err)
 			}
 
-			if predeployStatusResp.Status == porter_app.PredeployStatus_Failed || predeployStatusResp.Status == porter_app.PredeployStatus_Successful {
+			if predeployStatusResp.Status == porter_app.PredeployStatus_Failed {
+				eventStatus = types.PorterAppEventStatus_Failed
+				break
+			}
+			if predeployStatusResp.Status == porter_app.PredeployStatus_Successful {
 				break
 			}
 
 			time.Sleep(checkPredeployFrequency)
 		}
 
+		metadata := make(map[string]interface{})
+		metadata["end_time"] = time.Now().UTC()
+		_ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, eventID, eventStatus, metadata)
+
 		applyResp, err = client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, "", "", applyResp.AppRevisionId)
 		if err != nil {
 			return fmt.Errorf("apply error post-predeploy: %w", err)

BIN
dashboard/src/assets/cloud-formation-stack-complete.png


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

@@ -60,6 +60,17 @@ const AzureCredentialForm: React.FC<Props> = ({ goBack, proceed }) => {
             id: currentProject.id,
           });
         const azureIntegrationId = azureIntegrationResponse.data.cloud_provider_credentials_id;
+        try {
+          if (currentProject?.id != null) {
+            api.inviteAdmin(
+              "<token>",
+              {},
+              { project_id: currentProject?.id }
+            );
+          }
+        } catch (err) {
+          console.log(err);
+        }
         proceed(azureIntegrationId)
       } catch (err) {
         if (err.response?.data?.error) {

+ 225 - 168
dashboard/src/components/CloudFormationForm.tsx

@@ -1,9 +1,10 @@
-import React, { useState, useContext } from "react";
+import React, { useState, useContext, useMemo } from "react";
 import styled from "styled-components";
 import { v4 as uuidv4 } from 'uuid';
 
 import api from "shared/api";
 import aws from "assets/aws.png";
+import cloudformationStatus from "assets/cloud-formation-stack-complete.png";
 
 import { Context } from "shared/Context";
 
@@ -11,11 +12,13 @@ import Text from "./porter/Text";
 import Spacer from "./porter/Spacer";
 import Input from "./porter/Input";
 import Button from "./porter/Button";
-import Error from "./porter/Error";
-import Step from "./porter/Step";
 import Link from "./porter/Link";
 import Container from "./porter/Container";
-import VerticalSteps from "./porter/VerticalSteps";
+import Step from "./porter/Step";
+import { Box, Step as MuiStep, StepContent, StepLabel, Stepper, ThemeProvider, Typography, createTheme } from "@material-ui/core";
+import { useQuery } from "@tanstack/react-query";
+import Modal from "./porter/Modal";
+import theme from "shared/themes/midnight";
 
 type Props = {
   goBack: () => void;
@@ -23,17 +26,45 @@ type Props = {
   switchToCredentialFlow: () => void;
 };
 
+const stepperTheme = createTheme({
+  palette: {
+    background: {
+      paper: 'none',
+    },
+    text: {
+      primary: '#DFDFE1',
+      secondary: '#aaaabb',
+    },
+    action: {
+      active: '#001E3C',
+    },
+  },
+  typography: {
+    fontFamily: "Work Sans, sans-serif",
+  },
+  overrides: {
+    MuiStepIcon: {
+      root: {
+        '&$completed': {
+          color: theme.button,
+        },
+        '&$active': {
+          color: theme.button,
+        },
+      },
+    },
+  },
+});
+
 const CloudFormationForm: React.FC<Props> = ({
   goBack,
   proceed,
   switchToCredentialFlow
 }) => {
-  const [hasSentAWSNotif, setHasSentAWSNotif] = useState(false);
-  const [roleStatus, setRoleStatus] = useState("");
-  const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
   const [AWSAccountID, setAWSAccountID] = useState("");
-  const [AWSAccountIDInputError, setAWSAccountIDInputError] = useState<string | undefined>(undefined);
-  const [currentStep, setCurrentStep] = useState<number>(1);
+  const [currentStep, setCurrentStep] = useState<number>(0);
+  const [hasClickedCloudformationButton, setHasClickedCloudformationButton] = useState(false);
+  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
 
   const { currentProject } = useContext(Context);
   const markStepStarted = async (
@@ -55,6 +86,9 @@ const CloudFormationForm: React.FC<Props> = ({
       }
   ) => {
     try {
+      if (currentProject == null) {
+        return;
+      }
       await api.updateOnboardingStep("<token>", { step, account_id, cloudformation_url, error_message, login_url, external_id }, {
         project_id: currentProject.id,
       });
@@ -63,51 +97,83 @@ const CloudFormationForm: React.FC<Props> = ({
     }
   };
 
-  const getAccountIdInputError = (accountId: string) => {
+  const { data: canProceed } = useQuery(
+    ["createAWSIntegration", currentStep, hasClickedCloudformationButton, AWSAccountID],
+    async () => {
+      if (currentProject == null) {
+        return false;
+      };
+      let externalId = getExternalId();
+      let targetARN = `arn:aws:iam::${AWSAccountID}:role/porter-manager`
+      await api
+        .createAWSIntegration(
+          "<token>",
+          {
+            aws_target_arn: targetARN,
+            aws_external_id: externalId,
+          },
+          {
+            id: currentProject.id,
+          }
+        );
+      return true;
+    },
+    {
+      enabled: currentStep === 2,
+      retry: (failureCount, err) => {
+        // if they've waited over 35 seconds notify us on slack. Cloudformation stack should only take around 20-25 seconds to create
+        if (failureCount === 7 && hasClickedCloudformationButton) {
+          reportFailedCreateAWSIntegration();
+        }
+        return true;
+      },
+      retryDelay: 5000,
+    }
+  )
+
+  const awsAccountIdInputError = useMemo(() => {
     const regex = /^\d{12}$/;
-    if (accountId === "") {
+    if (AWSAccountID.trim().length === 0) {
       return undefined;
-    } else if (!regex.test(accountId)) {
+    } else if (!regex.test(AWSAccountID)) {
       return 'A valid AWS Account ID must be a 12-digit number.';
     }
     return undefined;
-  };
+  }, [AWSAccountID]);
 
   const handleAWSAccountIDChange = (accountId: string) => {
     setAWSAccountID(accountId);
+    setHasClickedCloudformationButton(false);
     if (accountId === "open-sesame") {
       switchToCredentialFlow();
     }
-    // handle case where user resets the input to empty
-    if (accountId.trim().length === 0) {
-      setCurrentStep(1);
-      setAWSAccountIDInputError(undefined);
-      return;
-    }
-    const accountIdInputError = getAccountIdInputError(accountId);
-    if (accountIdInputError == null) {
-      setCurrentStep(2);
-      if (!hasSentAWSNotif) {
-        setHasSentAWSNotif(true);
-        markStepStarted({ step: "aws-account-id-complete", account_id: accountId });
-        if (currentProject != null) {
-          try {
-            api.inviteAdmin(
-              "<token>",
-              {},
-              { project_id: currentProject.id }
-            );
-          } catch (err) {
-            console.log(err);
-          }
-        }
-      }
-    } else {
-      setCurrentStep(1);
-    }
-    setAWSAccountIDInputError(accountIdInputError);
   };
 
+  const handleContinueWithAWSAccountId = () => {
+    setCurrentStep(2);
+    markStepStarted({ step: "aws-account-id-complete", account_id: AWSAccountID });
+  }
+
+  const handleProceedToProvisionStep = () => {
+    try {
+      if (currentProject != null) {
+        api.inviteAdmin(
+          "<token>",
+          {},
+          { project_id: currentProject.id }
+        );
+      };
+    } catch (err) {
+      console.log(err);
+    }
+    markStepStarted({ step: "aws-create-integration-success", account_id: AWSAccountID })
+    proceed(`arn:aws:iam::${AWSAccountID}:role/porter-manager`);
+  }
+
+  const reportFailedCreateAWSIntegration = () => {
+    markStepStarted({ step: "aws-create-integration-failed", account_id: AWSAccountID, external_id: getExternalId() })
+  }
+
   const getExternalId = () => {
     let externalId = localStorage.getItem(AWSAccountID)
     if (!externalId) {
@@ -118,89 +184,54 @@ const CloudFormationForm: React.FC<Props> = ({
     return externalId
   }
 
-  const checkIfRoleExists = async () => {
-    let externalId = getExternalId();
-    let targetARN = `arn:aws:iam::${AWSAccountID}:role/porter-manager`
-
-    setRoleStatus("loading");
-    setErrorMessage(undefined)
-    try {
-      if (currentProject == null) {
-        setErrorMessage("Could not find current project.")
-        return;
-      };
-      await api
-        .createAWSIntegration(
-          "<token>",
-          {
-            aws_target_arn: targetARN,
-            aws_external_id: externalId,
-          },
-          {
-            id: currentProject.id,
-          }
-        );
-      setRoleStatus("successful")
-      markStepStarted({ step: "aws-create-integration-success", account_id: AWSAccountID })
-      proceed(targetARN);
-    } catch (err) {
-      setRoleStatus("");
-      setErrorMessage("Porter could not access your AWS account. Please make sure you have granted permissions and try again.")
-      markStepStarted({
-        step: "aws-create-integration-failure",
-        account_id: AWSAccountID,
-        error_message: err?.response?.data?.error ??
-          err?.toString() ?? "unable to determine error - check honeycomb",
-        external_id: externalId,
-      })
-    }
-  };
-
-  const directToAWSLoginAndProceedStep = () => {
+  const directToAWSLogin = () => {
     const login_url = `https://signin.aws.amazon.com/console`;
-    markStepStarted({ step: "aws-login-redirect-success", login_url })
-    window.open(login_url, "_blank")
+    markStepStarted({ step: "aws-login-redirect-success", login_url });
+    window.open(login_url, "_blank");
   }
 
-  const directToCloudFormationAndProceedStep = () => {
+  const directToCloudFormation = () => {
     const externalId = getExternalId();
     let trustArn = process.env.TRUST_ARN ? process.env.TRUST_ARN : "arn:aws:iam::108458755588:role/CAPIManagement";
     const cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole&param_ExternalIdParameter=${externalId}&param_TrustArnParameter=${trustArn}`
     markStepStarted({ step: "aws-cloudformation-redirect-success", account_id: AWSAccountID, cloudformation_url, external_id: externalId })
-    setCurrentStep(3);
     window.open(cloudformation_url, "_blank")
+    setHasClickedCloudformationButton(true);
   }
 
   const renderContent = () => {
     return (
       <>
-        <Text>Grant Porter permissions to create infrastructure in your AWS account by following 4 simple steps.</Text>
+        <Text>Grant Porter permissions to create infrastructure in your AWS account by following 3 simple steps.</Text>
         <Spacer y={1} />
-        <VerticalSteps
-          currentStep={currentStep}
-          steps={
-            [
-              <>
-                <Text size={16}>1. Log in to your AWS Account.</Text>
-                <Spacer y={0.25} />
-                <Text color="helper">Return to Porter after successful log-in.</Text>
+        <ThemeProvider theme={stepperTheme} >
+          <Stepper activeStep={currentStep} orientation="vertical" style={{ padding: 0 }}>
+            <MuiStep>
+              <StepLabel>Log in to your AWS Account.</StepLabel>
+              <StepContent>
+                <Text color="helper">Return to Porter after successful login.</Text>
                 <Spacer y={0.5} />
                 <AWSButtonContainer>
                   <ButtonImg src={aws} />
                   <Button
                     width={"170px"}
-                    onClick={directToAWSLoginAndProceedStep}
-                    color="#1E2631"
+                    onClick={directToAWSLogin}
+                    color="linear-gradient(180deg, #26292e, #24272c)"
                     withBorder
                   >
                     Log in
                   </Button>
                 </AWSButtonContainer>
-              </>,
-              <>
-                <Text size={16}>2. Provide your AWS Account ID.</Text>
-                <Spacer y={0.25} />
-                <Text color="helper">Make sure this is the ID of the account you are currently logged into, and would like to provision resources in.</Text>
+                <Spacer y={0.5} />
+                <StepChangeButtonsContainer>
+                  <Button onClick={() => setCurrentStep(1)}>Continue</Button>
+                </StepChangeButtonsContainer>
+              </StepContent>
+            </MuiStep>
+            <MuiStep>
+              <StepLabel>Enter your AWS Account ID.</StepLabel>
+              <StepContent>
+                <Text color="helper">Make sure this is the ID of the account you are currently logged into and would like to provision resources in.</Text>
                 <Spacer y={0.5} />
                 <Input
                   label={
@@ -219,86 +250,104 @@ const CloudFormationForm: React.FC<Props> = ({
                   value={AWSAccountID}
                   setValue={handleAWSAccountIDChange}
                   placeholder="ex: 915037676314"
-                  error={AWSAccountIDInputError}
+                  error={awsAccountIdInputError}
                 />
-              </>,
-              <>
-                <Text size={16}>3. Create an AWS Cloudformation Stack.</Text>
-                <Spacer y={0.25} />
-                <Text color="helper">This grants Porter permissions to create infrastructure.</Text>
-                <Spacer y={0.25} />
-                <Text color="helper">
-                  Return to Porter once the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE".
-                </Text>
+                <Spacer y={0.5} />
+                <StepChangeButtonsContainer>
+                  <Button onClick={handleContinueWithAWSAccountId} disabled={awsAccountIdInputError != null || AWSAccountID.length === 0}>Continue</Button>
+                  <Spacer inline x={0.5} />
+                  <Button onClick={() => setCurrentStep(0)} color="#121212">Back</Button>
+                </StepChangeButtonsContainer>
+              </StepContent>
+            </MuiStep>
+            <MuiStep>
+              <StepLabel optional={<Typography variant="caption" color="textSecondary">This grants Porter permissions to create infrastructure in your account.</Typography>}>Create an AWS Cloudformation Stack.</StepLabel>
+              <StepContent>
+                <Text color="helper">Clicking the button below will take you to the AWS CloudFormation console. Return to Porter after clicking 'Create stack' in the bottom right corner.</Text>
                 <Spacer y={0.5} />
                 <AWSButtonContainer>
                   <ButtonImg src={aws} />
                   <Button
                     width={"170px"}
-                    onClick={directToCloudFormationAndProceedStep}
-                    color="#1E2631"
+                    onClick={directToCloudFormation}
+                    color="linear-gradient(180deg, #26292e, #24272c)"
                     withBorder
+                    disabled={canProceed}
+                    disabledTooltipMessage={"Porter can already access your account!"}
                   >
                     Grant permissions
                   </Button>
                 </AWSButtonContainer>
-              </>,
-              <>
-                <Text size={16}>4. Continue to the provision step.</Text>
                 <Spacer y={0.5} />
-                <Button
-                  width={"200px"}
-                  onClick={checkIfRoleExists}
-                  status={
-                    errorMessage ? (
-                      <Error
-                        message={errorMessage}
-                        ctaText="Troubleshooting steps"
-                        errorModalContents={
-                          <>
-                            <Text size={16}>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}>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>
-              </>
-            ].filter(step => step != null)
-          }
-        />
+                <Text color="helper">
+                  Once the CloudFormation stack has status{" "}
+                  <Box component="span" color="#1d8102">
+                    CREATE_COMPLETE
+                  </Box>, you may proceed.
+                </Text>
+                <Spacer y={0.25} />
+                <Text color="helper">This may take 1 - 2 minutes.</Text>
+                <Spacer y={0.5} />
+                <StepChangeButtonsContainer>
+                  <Button onClick={handleProceedToProvisionStep} disabled={!canProceed}>Continue</Button>
+                  <Spacer inline x={0.5} />
+                  <Button
+                    onClick={() => setCurrentStep(1)}
+                    color="#121212"
+                    status={canProceed ? "success" : hasClickedCloudformationButton ? "loading" : undefined}
+                    loadingText={`Checking if Porter can access AWS account ID ${AWSAccountID}...`}
+                    successText={`Porter can access AWS account ID ${AWSAccountID}`}
+                  >
+                    Back
+                  </Button>
+                </StepChangeButtonsContainer>
+                <Spacer y={0.5} />
+                <Link hasunderline onClick={() => setShowNeedHelpModal(true)}>
+                  Need help?
+                </Link>
+              </StepContent>
+            </MuiStep>
+            {showNeedHelpModal &&
+              <Modal closeModal={() => setShowNeedHelpModal(false)} width={"800px"}>
+                <Text size={16}>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 CloudFormation, select "Create stack" on the bottom right.</Step>
+                <Spacer y={1} />
+                <Step number={5}>The stack will start to create. Refresh until the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE":</Step>
+                <Spacer y={1} />
+                <ImageDiv>
+                  <img src={cloudformationStatus} height="250px" />
+                </ImageDiv>
+                <Spacer y={1} />
+                <Step number={6}>Return to Porter and select "Continue".</Step>
+                <Spacer y={1} />
+                <Step number={7}>If you continue to see issues, <a href="mailto:support@porter.run">email support.</a></Step>
+              </Modal>
+            }
+          </Stepper>
+        </ThemeProvider>
       </>
     );
   }
@@ -324,6 +373,14 @@ const CloudFormationForm: React.FC<Props> = ({
 
 export default CloudFormationForm;
 
+const ImageDiv = styled.div`
+  text-align: center;
+`;
+
+const StepChangeButtonsContainer = styled.div`
+  display: flex;
+`;
+
 const Flex = styled.div`
   display: flex;
   ailgn-items: center;

+ 11 - 1
dashboard/src/components/GCPCredentialsForm.tsx

@@ -117,8 +117,18 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
 
 
   const saveCredentials = async () => {
-
     if (gcpCloudProviderCredentialID) {
+      try {
+        if (currentProject?.id != null) {
+          api.inviteAdmin(
+            "<token>",
+            {},
+            { project_id: currentProject?.id }
+          );
+        }
+      } catch (err) {
+        console.log(err);
+      }
       proceed(gcpCloudProviderCredentialID)
     }
 

+ 4 - 2
dashboard/src/components/GCPProvisionerSettings.tsx

@@ -41,6 +41,8 @@ import PreflightChecks from "./PreflightChecks";
 
 const locationOptions = [
   { value: "us-east1", label: "us-east1 (South Carolina, USA)" },
+  { value: "us-east4", label: "us-east4 (Virginia, USA)" },
+  { value: "us-central1", label: "us-central1 (Iowa, USA)" },
   { value: "asia-south1", label: "asia-south1 (Mumbia, India)" },
 ];
 
@@ -368,7 +370,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
       preflightChecks()
     }
 
-  }, [props.selectedClusterVersion, clusterNetworking]);
+  }, [props.selectedClusterVersion, clusterNetworking, region]);
 
   const preflightChecks = async () => {
     setIsLoading(true);
@@ -553,4 +555,4 @@ const ExpandHeader = styled.div<{ isExpanded: boolean }>`
     props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
     transition: transform 0.1s ease;
   }
-`;
+`;

+ 0 - 9
dashboard/src/components/ProvisionerSettings.tsx

@@ -10,8 +10,6 @@ import info from "assets/info-outlined.svg";
 
 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,
@@ -34,11 +32,8 @@ import Text from "./porter/Text";
 import Select from "./porter/Select";
 import Input from "./porter/Input";
 import Checkbox from "./porter/Checkbox";
-import { Certificate } from "crypto";
 import Tooltip from "./porter/Tooltip";
 import Icon from "./porter/Icon";
-import { set } from "traverse";
-import { load } from "js-yaml";
 import Loading from "./Loading";
 const regionOptions = [
   { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
@@ -82,7 +77,6 @@ const machineTypeOptions = [
 
 const clusterVersionOptions = [
   { value: "v1.24.0", label: "1.24.0" },
-  { value: "v1.25.0", label: "1.25.0" },
 ];
 
 type Props = RouteComponentProps & {
@@ -99,9 +93,7 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
     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.medium");
@@ -436,7 +428,6 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
           setAdditionalNodePolicies(nodeGroup.additionalPolicies);
         }
       });
-      setCreateStatus("");
       setClusterName(eksValues.clusterName);
       setAwsRegion(eksValues.region);
       setClusterVersion(eksValues.clusterVersion);

+ 27 - 1
dashboard/src/components/porter/Button.tsx

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
 import styled, { keyframes } from "styled-components";
 
 import loading from "assets/loading.gif";
+import Tooltip from "./Tooltip";
 
 type Props = {
   children: React.ReactNode;
@@ -19,6 +20,7 @@ type Props = {
   rounded?: boolean;
   alt?: boolean;
   type?: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
+  disabledTooltipMessage?: string;
 };
 
 const Button: React.FC<Props> = ({
@@ -37,6 +39,7 @@ const Button: React.FC<Props> = ({
   rounded,
   alt,
   type,
+  disabledTooltipMessage,
 }) => {
   const renderStatus = () => {
     switch (status) {
@@ -65,7 +68,30 @@ const Button: React.FC<Props> = ({
     }
   };
 
-  return (
+  return disabled && disabledTooltipMessage ? (
+    <Tooltip content={disabledTooltipMessage} position="right">
+      <Wrapper>
+        <StyledButton
+          disabled={disabled}
+          onClick={() => {
+            if (!disabled && onClick) {
+              onClick();
+            }
+          }}
+          width={width}
+          height={height}
+          color={color}
+          withBorder={withBorder || alt}
+          rounded={rounded || alt}
+          alt={alt}
+          type={type}
+        >
+          <Text>{children}</Text>
+        </StyledButton>
+        {(helperText || status) && renderStatus()}
+      </Wrapper>
+    </Tooltip>
+  ) : (
     <Wrapper>
       <StyledButton
         disabled={disabled}

+ 1 - 4
dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx

@@ -100,12 +100,9 @@ const ProjectSelectionModal: React.FC<Props> = ({
             setCurrentProject(project);
 
             const clusters_list = await updateClusterList(project.id);
-            console.log(clusters_list);
-
             if (clusters_list?.length > 0) {
               setCurrentCluster(clusters_list[0]);
               if (project.simplified_view_enabled) {
-                console.log("HERE BITCH")
                 pushFiltered(props, "/apps", ["project_id"], {});
               }
               else {
@@ -239,4 +236,4 @@ const I = styled.i`
   align-items: center;
   margin-right: 5px;
   justify-content: center;
-`;
+`;