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.
This commit is contained in:
parent
af19251f17
commit
a9d2ba59a8
4 changed files with 213 additions and 1 deletions
|
|
@ -119,7 +119,7 @@ const AwsSourceName = ({ id }: AwsSourceNamePropTypes) => {
|
|||
return <SourceNotFoundPopover />;
|
||||
};
|
||||
|
||||
const parseGcpSharedWith = (
|
||||
export const parseGcpSharedWith = (
|
||||
sharedWith: GcpUploadRequestOptions['share_with_accounts'],
|
||||
) => {
|
||||
if (sharedWith) {
|
||||
|
|
|
|||
|
|
@ -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' && (
|
||||
<GcpLaunchModal
|
||||
isOpen={isModalOpen}
|
||||
handleModalToggle={handleModalToggle}
|
||||
compose={compose}
|
||||
composeStatus={composeStatus}
|
||||
/>
|
||||
)}
|
||||
{!launchEofFlag && isModalOpen && (
|
||||
<Modal
|
||||
isOpen
|
||||
|
|
|
|||
169
src/Components/Launch/GcpLaunchModal.tsx
Normal file
169
src/Components/Launch/GcpLaunchModal.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
ClipboardCopy,
|
||||
ClipboardCopyVariant,
|
||||
List,
|
||||
ListComponent,
|
||||
ListItem,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalVariant,
|
||||
OrderType,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { generateDefaultName } from './useGenerateDefaultName';
|
||||
|
||||
import {
|
||||
ComposesResponseItem,
|
||||
ComposeStatus,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import {
|
||||
isGcpUploadRequestOptions,
|
||||
isGcpUploadStatus,
|
||||
} from '../../store/typeGuards';
|
||||
import { parseGcpSharedWith } from '../ImagesTable/ImageDetails';
|
||||
|
||||
type LaunchProps = {
|
||||
isOpen: boolean;
|
||||
handleModalToggle: (event: KeyboardEvent | React.MouseEvent) => 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 || '<your_project_id>'
|
||||
}`;
|
||||
const createInstance = `gcloud compute instances create ${uniqueImageName} --image=${uniqueImageName} --project=${
|
||||
customerProjectId || '<your_project_id>'
|
||||
}`;
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleModalToggle}
|
||||
variant={ModalVariant.large}
|
||||
aria-label='Open launch guide modal'
|
||||
>
|
||||
<ModalHeader
|
||||
title={'Launch with Google Cloud Platform'}
|
||||
labelId='modal-title'
|
||||
description={compose.image_name}
|
||||
/>
|
||||
<ModalBody id='modal-box-body-basic'>
|
||||
<List component={ListComponent.ol} type={OrderType.number}>
|
||||
<ListItem>
|
||||
Install the gcloud CLI. See the{' '}
|
||||
<Button
|
||||
component='a'
|
||||
target='_blank'
|
||||
variant='link'
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition='right'
|
||||
href={`https://cloud.google.com/sdk/docs/install`}
|
||||
className='pf-v6-u-pl-0'
|
||||
>
|
||||
Install gcloud CLI
|
||||
</Button>
|
||||
documentation.
|
||||
<ClipboardCopy isReadOnly hoverTip='Copy' clickTip='Copied'>
|
||||
{installationCommand}
|
||||
</ClipboardCopy>
|
||||
</ListItem>
|
||||
<ListItem>{authorizeString}</ListItem>
|
||||
<ListItem>
|
||||
Enter your GCP project ID, and run the command to create the image
|
||||
in your project.
|
||||
<TextInput
|
||||
className='pf-v6-u-mt-sm pf-v6-u-mb-md'
|
||||
value={customerProjectId}
|
||||
type='text'
|
||||
onChange={(_event, value) => setCustomerProjectId(value)}
|
||||
aria-label='Project ID input'
|
||||
placeholder='Project ID'
|
||||
/>
|
||||
<ClipboardCopy
|
||||
isReadOnly
|
||||
hoverTip='Copy'
|
||||
clickTip='Copied'
|
||||
variant={ClipboardCopyVariant.expansion}
|
||||
>
|
||||
{createImage}
|
||||
</ClipboardCopy>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Create an instance of your image by either accessing the{' '}
|
||||
<Button
|
||||
component='a'
|
||||
target='_blank'
|
||||
variant='link'
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition='right'
|
||||
href={`https://console.cloud.google.com/compute/images`}
|
||||
className='pf-v6-u-pl-0'
|
||||
>
|
||||
GCP console
|
||||
</Button>{' '}
|
||||
or by running the following command:
|
||||
<ClipboardCopy
|
||||
isReadOnly
|
||||
hoverTip='Copy'
|
||||
clickTip='Copied'
|
||||
variant={ClipboardCopyVariant.expansion}
|
||||
>
|
||||
{createInstance}
|
||||
</ClipboardCopy>
|
||||
</ListItem>
|
||||
</List>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button key='close' variant='primary' onClick={handleModalToggle}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
34
src/Components/Launch/useGenerateDefaultName.ts
Normal file
34
src/Components/Launch/useGenerateDefaultName.ts
Normal file
|
|
@ -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}`;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue