From 37889b57f08f9bbbc56751464dd586841aef5e43 Mon Sep 17 00:00:00 2001 From: Feroze Mohideen Date: Wed, 6 Mar 2024 12:08:20 -0500 Subject: [PATCH] support percent completion indication for cloud credential update (#4380) --- .../project_integration/create_aws.go | 1 + api/types/project_integration.go | 3 +- dashboard/src/lib/hooks/useCloudProvider.ts | 12 +- .../forms/aws/GrantAWSPermissions.tsx | 106 +++++++++++------- .../forms/azure/CreateAKSClusterForm.tsx | 8 +- go.mod | 2 +- go.sum | 2 + 7 files changed, 83 insertions(+), 51 deletions(-) diff --git a/api/server/handlers/project_integration/create_aws.go b/api/server/handlers/project_integration/create_aws.go index dd0615dcf6..9fc4bb26ea 100644 --- a/api/server/handlers/project_integration/create_aws.go +++ b/api/server/handlers/project_integration/create_aws.go @@ -139,6 +139,7 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } res.CloudProviderCredentialIdentifier = credResp.Msg.CredentialsIdentifier + res.PercentCompleted = credResp.Msg.PercentCompleted } else { credReq := porterv1.CreateAssumeRoleChainRequest{ //nolint:staticcheck // being deprecated by the above UpdateCloudProviderCredentials ProjectId: int64(project.ID), diff --git a/api/types/project_integration.go b/api/types/project_integration.go index 3773aa81db..2864009dde 100644 --- a/api/types/project_integration.go +++ b/api/types/project_integration.go @@ -96,7 +96,8 @@ type CreateAWSRequest struct { type CreateAWSResponse struct { *AWSIntegration - CloudProviderCredentialIdentifier string `json:"cloud_provider_credentials_id"` + CloudProviderCredentialIdentifier string `json:"cloud_provider_credentials_id"` + PercentCompleted float32 `json:"percent_completed"` } type OverwriteAWSRequest struct { diff --git a/dashboard/src/lib/hooks/useCloudProvider.ts b/dashboard/src/lib/hooks/useCloudProvider.ts index 5e2c06a292..4ba1a0495b 100644 --- a/dashboard/src/lib/hooks/useCloudProvider.ts +++ b/dashboard/src/lib/hooks/useCloudProvider.ts @@ -11,8 +11,8 @@ export const isAWSArnAccessible = async ({ targetArn: string; externalId: string; projectId: number; -}): Promise => { - await api.createAWSIntegration( +}): Promise => { + const res = await api.createAWSIntegration( "", { aws_target_arn: targetArn, @@ -20,7 +20,13 @@ export const isAWSArnAccessible = async ({ }, { id: projectId } ); - return true; + const parsed = await z + .object({ + percent_completed: z.number(), + }) + .parseAsync(res.data); + + return parsed.percent_completed; }; export const connectToAzureAccount = async ({ diff --git a/dashboard/src/main/home/infrastructure-dashboard/forms/aws/GrantAWSPermissions.tsx b/dashboard/src/main/home/infrastructure-dashboard/forms/aws/GrantAWSPermissions.tsx index 0f537be83d..6fdab951b7 100644 --- a/dashboard/src/main/home/infrastructure-dashboard/forms/aws/GrantAWSPermissions.tsx +++ b/dashboard/src/main/home/infrastructure-dashboard/forms/aws/GrantAWSPermissions.tsx @@ -13,6 +13,7 @@ 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 StatusBar from "components/porter/StatusBar"; import Text from "components/porter/Text"; import VerticalSteps from "components/porter/VerticalSteps"; import { type ButtonStatus } from "main/home/app-dashboard/app-view/AppDataContainer"; @@ -22,7 +23,6 @@ import { useClusterAnalytics } from "lib/hooks/useClusterAnalytics"; import { useIntercom } from "lib/hooks/useIntercom"; import GrantAWSPermissionsHelpModal from "../../modals/help/permissions/GrantAWSPermissionsHelpModal"; -import { CheckItem } from "../../modals/PreflightChecksModal"; type Props = { goBack: () => void; @@ -44,7 +44,8 @@ const GrantAWSPermissions: React.FC = ({ const [showNeedHelpModal, setShowNeedHelpModal] = useState(false); const [accountIdContinueButtonStatus, setAccountIdContinueButtonStatus] = useState(""); - const [isAccountAccessible, setIsAccountAccessible] = useState(false); + const [permissionsGrantCompletionPercentage, setAWSPermissionsProgress] = + useState(0); const { reportToAnalytics } = useClusterAnalytics(); const { showIntercomWithMessage } = useIntercom(); @@ -71,53 +72,74 @@ const GrantAWSPermissions: React.FC = ({ return externalId; }, [AWSAccountID, awsAccountIdInputError]); + const awsPermissionsLoadingMessage = useMemo(() => { + if (permissionsGrantCompletionPercentage === 0) { + return "Setting up access roles and policies..."; + } + if (permissionsGrantCompletionPercentage < 50) { + return "Creating cluster management roles..."; + } + if (permissionsGrantCompletionPercentage < 100) { + return "Creating cluster management policies..."; + } + return ""; + }, [permissionsGrantCompletionPercentage]); + const data = useQuery( [ "cloudFormationStackCreated", AWSAccountID, projectId, - isAccountAccessible, + permissionsGrantCompletionPercentage, externalId, ], async () => { try { - await isAWSArnAccessible({ + const res = await isAWSArnAccessible({ targetArn: `arn:aws:iam::${AWSAccountID}:role/porter-manager`, externalId, projectId, }); - return true; + return res; } catch (err) { - return false; + return 0; } }, { - enabled: currentStep === 3 && !isAccountAccessible, // no need to check if it's already accessible + enabled: + currentStep === 3 && permissionsGrantCompletionPercentage !== 100, // no need to check if it's already accessible refetchInterval: 5000, refetchIntervalInBackground: true, } ); useEffect(() => { if (data.isSuccess) { - setIsAccountAccessible(data.data); + setAWSPermissionsProgress(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 + setAWSPermissionsProgress(0); // any time they change the account ID, we need to re-check if it's accessible }; const checkIfAlreadyAccessible = async (): Promise => { setAccountIdContinueButtonStatus("loading"); try { - await isAWSArnAccessible({ + const awsIntegrationPercentCompleted = await isAWSArnAccessible({ targetArn: `arn:aws:iam::${AWSAccountID}:role/porter-manager`, externalId, projectId, }); - setCurrentStep(3); - setIsAccountAccessible(true); + if (awsIntegrationPercentCompleted > 0) { + // this indicates the permission check is already in place; no need to re-create cloudformation stack + setCurrentStep(3); + setAWSPermissionsProgress(awsIntegrationPercentCompleted); + setAccountIdContinueButtonStatus(""); + } else { + setCurrentStep(2); + setAccountIdContinueButtonStatus(""); + } } catch (err) { let shouldProceed = true; if (axios.isAxiosError(err)) { @@ -266,6 +288,7 @@ const GrantAWSPermissions: React.FC = ({ setCurrentStep(0); setAccountIdContinueButtonStatus(""); setAWSAccountID(""); + setAWSPermissionsProgress(0); }} color="#222222" > @@ -308,7 +331,7 @@ const GrantAWSPermissions: React.FC = ({ onClick={directToCloudFormation} color="linear-gradient(180deg, #26292e, #24272c)" withBorder - disabled={isAccountAccessible} + disabled={permissionsGrantCompletionPercentage === 100} disabledTooltipMessage={ "Porter can already access your account!" } @@ -338,36 +361,33 @@ const GrantAWSPermissions: React.FC = ({ , <> Check permissions - - - Checking if Porter can access AWS account with ID {AWSAccountID} - . This can take up to 10 minutes. - - { - setShowNeedHelpModal(true); - }} - > - Need help? - - - {isAccountAccessible ? ( - - ) : ( - - )} + + + { + showIntercomWithMessage({ + message: "I need help with AWS permissions setup.", + delaySeconds: 0, + }); + }} + > + Need help? + diff --git a/dashboard/src/main/home/infrastructure-dashboard/forms/azure/CreateAKSClusterForm.tsx b/dashboard/src/main/home/infrastructure-dashboard/forms/azure/CreateAKSClusterForm.tsx index 5861ee6d81..9803f45568 100644 --- a/dashboard/src/main/home/infrastructure-dashboard/forms/azure/CreateAKSClusterForm.tsx +++ b/dashboard/src/main/home/infrastructure-dashboard/forms/azure/CreateAKSClusterForm.tsx @@ -27,9 +27,11 @@ const CreateAKSClusterForm: React.FC = ({ const { reportToAnalytics } = useClusterAnalytics(); useEffect(() => { - const projectNameLimit = 31 - "-cluster-".length - 6; // 6 characters for the random suffix - const truncatedProjectName = projectName.substring(0, projectNameLimit); - const clusterName = `${truncatedProjectName}-cluster-${Math.random() + const truncatedProjectName = projectName + .substring(0, 24) + .replace(/-+$/, ""); + + const clusterName = `${truncatedProjectName}-${Math.random() .toString(36) .substring(2, 8)}`; diff --git a/go.mod b/go.mod index f237f89f62..298c5a76f4 100644 --- a/go.mod +++ b/go.mod @@ -83,7 +83,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.2.113 + github.com/porter-dev/api-contracts v0.2.114 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 diff --git a/go.sum b/go.sum index 8ad6491a21..3af4b02aac 100644 --- a/go.sum +++ b/go.sum @@ -1525,6 +1525,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= github.com/porter-dev/api-contracts v0.2.113 h1:sv1huO9MpykJaWhV2D5zTD2LouMbRSBV5ATt/5Ukrbo= github.com/porter-dev/api-contracts v0.2.113/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= +github.com/porter-dev/api-contracts v0.2.114 h1:qfEq70BJ8xXTkiZU7ygzOSGnMCqJHOa5Lbkfu4OzQBI= +github.com/porter-dev/api-contracts v0.2.114/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=