Skip to content

Commit a9d2ba5

Browse files
avitovaregexowl
authored andcommitted
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.
1 parent af19251 commit a9d2ba5

File tree

4 files changed

+213
-1
lines changed

4 files changed

+213
-1
lines changed

src/Components/ImagesTable/ImageDetails.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const AwsSourceName = ({ id }: AwsSourceNamePropTypes) => {
119119
return <SourceNotFoundPopover />;
120120
};
121121

122-
const parseGcpSharedWith = (
122+
export const parseGcpSharedWith = (
123123
sharedWith: GcpUploadRequestOptions['share_with_accounts'],
124124
) => {
125125
if (sharedWith) {

src/Components/ImagesTable/Instance.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { resolveRelPath } from '../../Utilities/path';
5757
import { useFlag } from '../../Utilities/useGetEnvironment';
5858
import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions';
5959
import { AWSLaunchModal } from '../Launch/AWSLaunchModal';
60+
import { GcpLaunchModal } from '../Launch/GcpLaunchModal';
6061

6162
type CloudInstancePropTypes = {
6263
compose: ComposesResponseItem;
@@ -224,6 +225,14 @@ const ProvisioningLink = ({
224225
composeStatus={composeStatus}
225226
/>
226227
)}
228+
{launchEofFlag && isModalOpen && provider === 'gcp' && (
229+
<GcpLaunchModal
230+
isOpen={isModalOpen}
231+
handleModalToggle={handleModalToggle}
232+
compose={compose}
233+
composeStatus={composeStatus}
234+
/>
235+
)}
227236
{!launchEofFlag && isModalOpen && (
228237
<Modal
229238
isOpen
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import React, { useState } from 'react';
2+
3+
import {
4+
Button,
5+
ClipboardCopy,
6+
ClipboardCopyVariant,
7+
List,
8+
ListComponent,
9+
ListItem,
10+
Modal,
11+
ModalBody,
12+
ModalFooter,
13+
ModalHeader,
14+
ModalVariant,
15+
OrderType,
16+
TextInput,
17+
} from '@patternfly/react-core';
18+
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
19+
20+
import { generateDefaultName } from './useGenerateDefaultName';
21+
22+
import {
23+
ComposesResponseItem,
24+
ComposeStatus,
25+
} from '../../store/imageBuilderApi';
26+
import {
27+
isGcpUploadRequestOptions,
28+
isGcpUploadStatus,
29+
} from '../../store/typeGuards';
30+
import { parseGcpSharedWith } from '../ImagesTable/ImageDetails';
31+
32+
type LaunchProps = {
33+
isOpen: boolean;
34+
handleModalToggle: (event: KeyboardEvent | React.MouseEvent) => void;
35+
compose: ComposesResponseItem;
36+
composeStatus: ComposeStatus | undefined;
37+
};
38+
39+
export const GcpLaunchModal = ({
40+
isOpen,
41+
handleModalToggle,
42+
compose,
43+
composeStatus,
44+
}: LaunchProps) => {
45+
const [customerProjectId, setCustomerProjectId] = useState('');
46+
47+
const statusOptions = composeStatus?.image_status.upload_status?.options;
48+
const composeOptions =
49+
compose.request.image_requests[0].upload_request.options;
50+
51+
if (
52+
(statusOptions && !isGcpUploadStatus(statusOptions)) ||
53+
!isGcpUploadRequestOptions(composeOptions)
54+
) {
55+
throw TypeError(
56+
`Error: options must be of type GcpUploadRequestOptions, not ${typeof statusOptions}.`,
57+
);
58+
}
59+
60+
const imageName = statusOptions?.image_name;
61+
const projectId = statusOptions?.project_id;
62+
if (!imageName || !projectId) {
63+
throw TypeError(
64+
`Error: Image name not found, unable to generate a command to copy ${typeof statusOptions}.`,
65+
);
66+
}
67+
const uniqueImageName = generateDefaultName(imageName);
68+
const authorizeString =
69+
composeOptions.share_with_accounts &&
70+
composeOptions.share_with_accounts.length === 1
71+
? `Authorize gcloud CLI to the following
72+
account: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.`
73+
: composeOptions.share_with_accounts
74+
? `Authorize gcloud CLI to use one of the following
75+
accounts: ${parseGcpSharedWith(composeOptions.share_with_accounts)}.`
76+
: 'Authorize gcloud CLI to use the account that the image is shared with.';
77+
const installationCommand = `sudo dnf install google-cloud-cli`;
78+
const createImage = `gcloud compute images create ${uniqueImageName} --source-image=${imageName} --source-image-project=${projectId} --project=${
79+
customerProjectId || '<your_project_id>'
80+
}`;
81+
const createInstance = `gcloud compute instances create ${uniqueImageName} --image=${uniqueImageName} --project=${
82+
customerProjectId || '<your_project_id>'
83+
}`;
84+
return (
85+
<Modal
86+
isOpen={isOpen}
87+
onClose={handleModalToggle}
88+
variant={ModalVariant.large}
89+
aria-label='Open launch guide modal'
90+
>
91+
<ModalHeader
92+
title={'Launch with Google Cloud Platform'}
93+
labelId='modal-title'
94+
description={compose.image_name}
95+
/>
96+
<ModalBody id='modal-box-body-basic'>
97+
<List component={ListComponent.ol} type={OrderType.number}>
98+
<ListItem>
99+
Install the gcloud CLI. See the{' '}
100+
<Button
101+
component='a'
102+
target='_blank'
103+
variant='link'
104+
icon={<ExternalLinkAltIcon />}
105+
iconPosition='right'
106+
href={`https://cloud.google.com/sdk/docs/install`}
107+
className='pf-v6-u-pl-0'
108+
>
109+
Install gcloud CLI
110+
</Button>
111+
documentation.
112+
<ClipboardCopy isReadOnly hoverTip='Copy' clickTip='Copied'>
113+
{installationCommand}
114+
</ClipboardCopy>
115+
</ListItem>
116+
<ListItem>{authorizeString}</ListItem>
117+
<ListItem>
118+
Enter your GCP project ID, and run the command to create the image
119+
in your project.
120+
<TextInput
121+
className='pf-v6-u-mt-sm pf-v6-u-mb-md'
122+
value={customerProjectId}
123+
type='text'
124+
onChange={(_event, value) => setCustomerProjectId(value)}
125+
aria-label='Project ID input'
126+
placeholder='Project ID'
127+
/>
128+
<ClipboardCopy
129+
isReadOnly
130+
hoverTip='Copy'
131+
clickTip='Copied'
132+
variant={ClipboardCopyVariant.expansion}
133+
>
134+
{createImage}
135+
</ClipboardCopy>
136+
</ListItem>
137+
<ListItem>
138+
Create an instance of your image by either accessing the{' '}
139+
<Button
140+
component='a'
141+
target='_blank'
142+
variant='link'
143+
icon={<ExternalLinkAltIcon />}
144+
iconPosition='right'
145+
href={`https://console.cloud.google.com/compute/images`}
146+
className='pf-v6-u-pl-0'
147+
>
148+
GCP console
149+
</Button>{' '}
150+
or by running the following command:
151+
<ClipboardCopy
152+
isReadOnly
153+
hoverTip='Copy'
154+
clickTip='Copied'
155+
variant={ClipboardCopyVariant.expansion}
156+
>
157+
{createInstance}
158+
</ClipboardCopy>
159+
</ListItem>
160+
</List>
161+
</ModalBody>
162+
<ModalFooter>
163+
<Button key='close' variant='primary' onClick={handleModalToggle}>
164+
Close
165+
</Button>
166+
</ModalFooter>
167+
</Modal>
168+
);
169+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export const generateDefaultName = (imageName: string) => {
2+
const date = new Date();
3+
const day = date.getDate().toString().padStart(2, '0');
4+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
5+
const year = date.getFullYear().toString();
6+
const hours = date.getHours().toString().padStart(2, '0');
7+
const minutes = date.getMinutes().toString().padStart(2, '0');
8+
9+
const dateTimeString = `${month}${day}${year}-${hours}${minutes}`;
10+
11+
// gcloud images are valid in the form of: (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)
12+
let newBlueprintName = imageName
13+
.toLowerCase()
14+
.replace(/[^a-z0-9-]/g, '-')
15+
.replace(/-{2,}/g, '-')
16+
.replace(/^-+|-+$/g, '');
17+
18+
if (!/^[a-z]/.test(newBlueprintName)) {
19+
newBlueprintName = 'i' + newBlueprintName;
20+
}
21+
22+
const maxLength = 63;
23+
const uniquePartLength = dateTimeString.length + 1;
24+
const baseNameMaxLength = maxLength - uniquePartLength;
25+
if (newBlueprintName.length > baseNameMaxLength) {
26+
newBlueprintName = newBlueprintName.substring(0, baseNameMaxLength);
27+
}
28+
29+
while (newBlueprintName.endsWith('-')) {
30+
newBlueprintName = newBlueprintName.slice(0, -1);
31+
}
32+
33+
return `${newBlueprintName}-${dateTimeString}`;
34+
};

0 commit comments

Comments
 (0)