From a9d2ba59a867b57638ac601becc05640e7761c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anna=20V=C3=ADtov=C3=A1?= Date: Mon, 11 Aug 2025 16:06:21 +0200 Subject: [PATCH] Launch: implement guidance for GCP (HMS-9004) This commit adds launch modal for guiding users through launching a GCP instance from their image. This commit also adds unique image name in the command in the clipboard. That way, users can rebuild the image more times without worrying about duplicate names. This guidance should be as helpful to users as possible, so even if they are able to create their own image name here, we chose it for them for the sake of simplicity. --- src/Components/ImagesTable/ImageDetails.tsx | 2 +- src/Components/ImagesTable/Instance.tsx | 9 + src/Components/Launch/GcpLaunchModal.tsx | 169 ++++++++++++++++++ .../Launch/useGenerateDefaultName.ts | 34 ++++ 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/Components/Launch/GcpLaunchModal.tsx create mode 100644 src/Components/Launch/useGenerateDefaultName.ts diff --git a/src/Components/ImagesTable/ImageDetails.tsx b/src/Components/ImagesTable/ImageDetails.tsx index d46b7263..809b0394 100644 --- a/src/Components/ImagesTable/ImageDetails.tsx +++ b/src/Components/ImagesTable/ImageDetails.tsx @@ -119,7 +119,7 @@ const AwsSourceName = ({ id }: AwsSourceNamePropTypes) => { return ; }; -const parseGcpSharedWith = ( +export const parseGcpSharedWith = ( sharedWith: GcpUploadRequestOptions['share_with_accounts'], ) => { if (sharedWith) { diff --git a/src/Components/ImagesTable/Instance.tsx b/src/Components/ImagesTable/Instance.tsx index c876a6a5..4af6f5e9 100644 --- a/src/Components/ImagesTable/Instance.tsx +++ b/src/Components/ImagesTable/Instance.tsx @@ -57,6 +57,7 @@ import { resolveRelPath } from '../../Utilities/path'; import { useFlag } from '../../Utilities/useGetEnvironment'; import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions'; import { AWSLaunchModal } from '../Launch/AWSLaunchModal'; +import { GcpLaunchModal } from '../Launch/GcpLaunchModal'; type CloudInstancePropTypes = { compose: ComposesResponseItem; @@ -224,6 +225,14 @@ const ProvisioningLink = ({ composeStatus={composeStatus} /> )} + {launchEofFlag && isModalOpen && provider === 'gcp' && ( + + )} {!launchEofFlag && isModalOpen && ( void; + compose: ComposesResponseItem; + composeStatus: ComposeStatus | undefined; +}; + +export const GcpLaunchModal = ({ + isOpen, + handleModalToggle, + compose, + composeStatus, +}: LaunchProps) => { + const [customerProjectId, setCustomerProjectId] = useState(''); + + const statusOptions = composeStatus?.image_status.upload_status?.options; + const composeOptions = + compose.request.image_requests[0].upload_request.options; + + if ( + (statusOptions && !isGcpUploadStatus(statusOptions)) || + !isGcpUploadRequestOptions(composeOptions) + ) { + throw TypeError( + `Error: options must be of type GcpUploadRequestOptions, not ${typeof statusOptions}.`, + ); + } + + const imageName = statusOptions?.image_name; + const projectId = statusOptions?.project_id; + if (!imageName || !projectId) { + throw TypeError( + `Error: Image name not found, unable to generate a command to copy ${typeof statusOptions}.`, + ); + } + const uniqueImageName = generateDefaultName(imageName); + const authorizeString = + composeOptions.share_with_accounts && + composeOptions.share_with_accounts.length === 1 + ? `Authorize gcloud CLI to the following + account: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.` + : composeOptions.share_with_accounts + ? `Authorize gcloud CLI to use one of the following + accounts: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.` + : 'Authorize gcloud CLI to use the account that the image is shared with.'; + const installationCommand = `sudo dnf install google-cloud-cli`; + const createImage = `gcloud compute images create ${uniqueImageName} --source-image=${imageName} --source-image-project=${projectId} --project=${ + customerProjectId || '' + }`; + const createInstance = `gcloud compute instances create ${uniqueImageName} --image=${uniqueImageName} --project=${ + customerProjectId || '' + }`; + return ( + + + + + + Install the gcloud CLI. See the{' '} + + documentation. + + {installationCommand} + + + {authorizeString} + + Enter your GCP project ID, and run the command to create the image + in your project. + setCustomerProjectId(value)} + aria-label='Project ID input' + placeholder='Project ID' + /> + + {createImage} + + + + Create an instance of your image by either accessing the{' '} + {' '} + or by running the following command: + + {createInstance} + + + + + + + + + ); +}; diff --git a/src/Components/Launch/useGenerateDefaultName.ts b/src/Components/Launch/useGenerateDefaultName.ts new file mode 100644 index 00000000..ac7a2cc6 --- /dev/null +++ b/src/Components/Launch/useGenerateDefaultName.ts @@ -0,0 +1,34 @@ +export const generateDefaultName = (imageName: string) => { + const date = new Date(); + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear().toString(); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + const dateTimeString = `${month}${day}${year}-${hours}${minutes}`; + + // gcloud images are valid in the form of: (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?) + let newBlueprintName = imageName + .toLowerCase() + .replace(/[^a-z0-9-]/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^-+|-+$/g, ''); + + if (!/^[a-z]/.test(newBlueprintName)) { + newBlueprintName = 'i' + newBlueprintName; + } + + const maxLength = 63; + const uniquePartLength = dateTimeString.length + 1; + const baseNameMaxLength = maxLength - uniquePartLength; + if (newBlueprintName.length > baseNameMaxLength) { + newBlueprintName = newBlueprintName.substring(0, baseNameMaxLength); + } + + while (newBlueprintName.endsWith('-')) { + newBlueprintName = newBlueprintName.slice(0, -1); + } + + return `${newBlueprintName}-${dateTimeString}`; +};