Browse Source

fail cloud formation flow on specific error (#4356)

Feroze Mohideen 2 years ago
parent
commit
dc880cd6a4

+ 9 - 13
dashboard/src/lib/hooks/useCloudProvider.ts

@@ -12,19 +12,15 @@ export const isAWSArnAccessible = async ({
   externalId: string;
   projectId: number;
 }): Promise<boolean> => {
-  try {
-    await api.createAWSIntegration(
-      "<token>",
-      {
-        aws_target_arn: targetArn,
-        aws_external_id: externalId,
-      },
-      { id: projectId }
-    );
-    return true;
-  } catch (err) {
-    return false;
-  }
+  await api.createAWSIntegration(
+    "<token>",
+    {
+      aws_target_arn: targetArn,
+      aws_external_id: externalId,
+    },
+    { id: projectId }
+  );
+  return true;
 };
 
 export const connectToAzureAccount = async ({

+ 67 - 21
dashboard/src/main/home/infrastructure-dashboard/forms/aws/GrantAWSPermissions.tsx

@@ -1,20 +1,25 @@
 import React, { useCallback, useEffect, useMemo, useState } from "react";
 import { useQuery } from "@tanstack/react-query";
+import axios from "axios";
 import styled from "styled-components";
 import { v4 as uuidv4 } from "uuid";
+import { z } from "zod";
 
 import Back from "components/porter/Back";
 import Button from "components/porter/Button";
 import Container from "components/porter/Container";
+import { Error as ErrorComponent } from "components/porter/Error";
 import Image from "components/porter/Image";
 import Input from "components/porter/Input";
 import Link from "components/porter/Link";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import VerticalSteps from "components/porter/VerticalSteps";
+import { type ButtonStatus } from "main/home/app-dashboard/app-view/AppDataContainer";
 import { CloudProviderAWS } from "lib/clusters/constants";
 import { isAWSArnAccessible } from "lib/hooks/useCloudProvider";
 import { useClusterAnalytics } from "lib/hooks/useClusterAnalytics";
+import { useIntercom } from "lib/hooks/useIntercom";
 
 import GrantAWSPermissionsHelpModal from "../../modals/help/permissions/GrantAWSPermissionsHelpModal";
 import { CheckItem } from "../../modals/PreflightChecksModal";
@@ -38,9 +43,11 @@ const GrantAWSPermissions: React.FC<Props> = ({
   const [currentStep, setCurrentStep] = useState<number>(0);
   const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
   const [accountIdContinueButtonStatus, setAccountIdContinueButtonStatus] =
-    useState<string>("");
+    useState<ButtonStatus>("");
   const [isAccountAccessible, setIsAccountAccessible] = useState(false);
   const { reportToAnalytics } = useClusterAnalytics();
+  const { showIntercomWithMessage } = useIntercom();
+
   const awsAccountIdInputError = useMemo(() => {
     const regex = /^\d{12}$/;
     if (AWSAccountID.trim().length === 0) {
@@ -50,6 +57,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
     }
     return undefined;
   }, [AWSAccountID]);
+
   const externalId = useMemo(() => {
     if (!AWSAccountID || awsAccountIdInputError) {
       return "";
@@ -62,6 +70,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
 
     return externalId;
   }, [AWSAccountID, awsAccountIdInputError]);
+
   const data = useQuery(
     [
       "cloudFormationStackCreated",
@@ -71,11 +80,16 @@ const GrantAWSPermissions: React.FC<Props> = ({
       externalId,
     ],
     async () => {
-      return await isAWSArnAccessible({
-        targetArn: `arn:aws:iam::${AWSAccountID}:role/porter-manager`,
-        externalId,
-        projectId,
-      });
+      try {
+        await isAWSArnAccessible({
+          targetArn: `arn:aws:iam::${AWSAccountID}:role/porter-manager`,
+          externalId,
+          projectId,
+        });
+        return true;
+      } catch (err) {
+        return false;
+      }
     },
     {
       enabled: currentStep === 3 && !isAccountAccessible, // no need to check if it's already accessible
@@ -88,30 +102,58 @@ const GrantAWSPermissions: React.FC<Props> = ({
       setIsAccountAccessible(data.data);
     }
   }, [data]);
+
   const handleAWSAccountIDChange = (accountId: string): void => {
     setAWSAccountID(accountId);
     setIsAccountAccessible(false); // any time they change the account ID, we need to re-check if it's accessible
   };
+
   const checkIfAlreadyAccessible = async (): Promise<void> => {
     setAccountIdContinueButtonStatus("loading");
-    const isAlreadyAccessible = await isAWSArnAccessible({
-      targetArn: `arn:aws:iam::${AWSAccountID}:role/porter-manager`,
-      externalId,
-      projectId,
-    });
-    if (isAlreadyAccessible) {
+    try {
+      await isAWSArnAccessible({
+        targetArn: `arn:aws:iam::${AWSAccountID}:role/porter-manager`,
+        externalId,
+        projectId,
+      });
       setCurrentStep(3);
       setIsAccountAccessible(true);
-    } else {
-      setCurrentStep(2);
+    } catch (err) {
+      let shouldProceed = true;
+      if (axios.isAxiosError(err)) {
+        const parsed = z
+          .object({ error: z.string() })
+          .safeParse(err.response?.data);
+        if (
+          parsed.success &&
+          parsed.data.error.includes(
+            "user does not have access to all projects"
+          )
+        ) {
+          setAccountIdContinueButtonStatus(
+            <ErrorComponent
+              message={"Unable to proceed. Please reach out to support."}
+            />
+          );
+          showIntercomWithMessage({
+            message: "I need help granting AWS permissions.",
+          });
+          shouldProceed = false;
+        }
+      }
+      if (shouldProceed) {
+        setCurrentStep(2);
+        setAccountIdContinueButtonStatus("");
+      }
+    } finally {
+      void reportToAnalytics({
+        projectId,
+        step: "aws-account-id-complete",
+        awsAccountId: AWSAccountID,
+      });
     }
-    void reportToAnalytics({
-      projectId,
-      step: "aws-account-id-complete",
-      awsAccountId: AWSAccountID,
-    });
-    setAccountIdContinueButtonStatus("");
   };
+
   const directToAWSLogin = (): void => {
     const loginUrl = `https://signin.aws.amazon.com/console`;
     void reportToAnalytics({
@@ -121,6 +163,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
     });
     window.open(loginUrl, "_blank");
   };
+
   const directToCloudFormation = useCallback(async () => {
     const trustArn = process.env.TRUST_ARN
       ? process.env.TRUST_ARN
@@ -136,6 +179,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
     setCurrentStep(3);
     window.open(cloudFormationUrl, "_blank");
   }, [AWSAccountID, externalId]);
+
   const handleGrantPermissionsComplete = (): void => {
     proceed({
       cloudProviderCredentialIdentifier: `arn:aws:iam::${AWSAccountID}:role/porter-manager`,
@@ -220,6 +264,8 @@ const GrantAWSPermissions: React.FC<Props> = ({
               <Button
                 onClick={() => {
                   setCurrentStep(0);
+                  setAccountIdContinueButtonStatus("");
+                  setAWSAccountID("");
                 }}
                 color="#222222"
               >
@@ -232,7 +278,7 @@ const GrantAWSPermissions: React.FC<Props> = ({
                 disabled={
                   awsAccountIdInputError != null ||
                   AWSAccountID.length === 0 ||
-                  accountIdContinueButtonStatus === "loading"
+                  accountIdContinueButtonStatus !== ""
                 }
                 status={accountIdContinueButtonStatus}
                 loadingText={`Checking if Porter can already access this account`}