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}`; +};