debian-image-builder-frontend/src/Components/CreateImageWizard/CreateImageWizard.js
lucasgarfield 53ce67ab47 Wizard: Add beta flag for AWS sources
This commit makes the new AWS sources feature only available in beta.

Note that the RTKQ hooks related to AWS sources are called in several
places outside of the AWS Target step (a prefetch on the Image Output
step and useQuery hook on the review step) but have not been hidden
behind beta flags - this should not present any problems and will make
exposing this feature in stable much easier when the time comes.
2023-03-01 11:25:28 +01:00

607 lines
18 KiB
JavaScript

import React, { useEffect } from 'react';
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import { useDispatch } from 'react-redux';
import { useLocation, useNavigate } from 'react-router-dom';
import ImageCreator from './ImageCreator';
import {
awsTargetStable,
awsTargetBeta,
fileSystemConfiguration,
googleCloudTarger,
imageName,
imageOutput,
msAzureTarget,
packages,
packagesContentSources,
registration,
repositories,
review,
} from './steps';
import {
fileSystemConfigurationValidator,
targetEnvironmentValidator,
} from './validators';
import './CreateImageWizard.scss';
import api from '../../api';
import { UNIT_GIB, UNIT_KIB, UNIT_MIB } from '../../constants';
import { getDistroRepoUrls } from '../../repos';
import { composeAdded } from '../../store/composesSlice';
import { fetchRepositories } from '../../store/repositoriesSlice';
import isRhel from '../../Utilities/isRhel';
import { resolveRelPath } from '../../Utilities/path';
import DocumentationButton from '../sharedComponents/DocumentationButton';
const handleKeyDown = (e, handleClose) => {
if (e.key === 'Escape') {
handleClose();
}
};
const onSave = (values) => {
const customizations = {
packages: values['selected-packages']?.map((p) => p.name),
};
if (values['custom-repositories']?.length > 0) {
customizations['payload_repositories'] = [...values['custom-repositories']];
}
if (values['register-system'] === 'register-now-insights') {
customizations.subscription = {
'activation-key': values['subscription-activation-key'],
insights: true,
organization: Number(values['subscription-organization-id']),
'server-url': values['subscription-server-url'],
'base-url': values['subscription-base-url'],
};
} else if (values['register-system'] === 'register-now') {
customizations.subscription = {
'activation-key': values['subscription-activation-key'],
insights: false,
organization: Number(values['subscription-organization-id']),
'server-url': values['subscription-server-url'],
'base-url': values['subscription-base-url'],
};
}
if (values['file-system-config-radio'] === 'manual') {
customizations.filesystem = [];
for (const fsc of values['file-system-configuration']) {
customizations.filesystem.push({
mountpoint: fsc.mountpoint,
min_size: fsc.size * fsc.unit,
});
}
}
const requests = [];
if (values['target-environment']?.aws) {
const options =
values['aws-target-type'] === 'aws-target-type-source'
? { share_with_sources: [values['aws-sources-select']] }
: { share_with_accounts: [values['aws-account-id']] };
const request = {
distribution: values.release,
image_name: values?.['image-name'],
image_requests: [
{
architecture: 'x86_64',
image_type: 'aws',
upload_request: {
type: 'aws',
options: options,
},
},
],
customizations,
};
requests.push(request);
}
if (values['target-environment']?.gcp) {
let share = '';
switch (values['google-account-type']) {
case 'googleAccount':
share = `user:${values['google-email']}`;
break;
case 'serviceAccount':
share = `serviceAccount:${values['google-email']}`;
break;
case 'googleGroup':
share = `group:${values['google-email']}`;
break;
case 'domain':
share = `domain:${values['google-domain']}`;
break;
}
const request = {
distribution: values.release,
image_name: values?.['image-name'],
image_requests: [
{
architecture: 'x86_64',
image_type: 'gcp',
upload_request: {
type: 'gcp',
options: {
share_with_accounts: [share],
},
},
},
],
customizations,
};
requests.push(request);
}
if (values['target-environment']?.azure) {
const request = {
distribution: values.release,
image_name: values?.['image-name'],
image_requests: [
{
architecture: 'x86_64',
image_type: 'azure',
upload_request: {
type: 'azure',
options: {
tenant_id: values['azure-tenant-id'],
subscription_id: values['azure-subscription-id'],
resource_group: values['azure-resource-group'],
},
},
},
],
customizations,
};
requests.push(request);
}
if (values['target-environment']?.vsphere) {
const request = {
distribution: values.release,
image_name: values?.['image-name'],
image_requests: [
{
architecture: 'x86_64',
image_type: 'vsphere',
upload_request: {
type: 'aws.s3',
options: {},
},
},
],
customizations,
};
requests.push(request);
}
if (values['target-environment']?.['guest-image']) {
const request = {
distribution: values.release,
image_name: values?.['image-name'],
image_requests: [
{
architecture: 'x86_64',
image_type: 'guest-image',
upload_request: {
type: 'aws.s3',
options: {},
},
},
],
customizations,
};
requests.push(request);
}
if (values['target-environment']?.['image-installer']) {
const request = {
distribution: values.release,
image_name: values?.['image-name'],
image_requests: [
{
architecture: 'x86_64',
image_type: 'image-installer',
upload_request: {
type: 'aws.s3',
options: {},
},
},
],
customizations,
};
requests.push(request);
}
return requests;
};
const parseSizeUnit = (bytesize) => {
let size;
let unit;
if (bytesize % UNIT_GIB === 0) {
size = bytesize / UNIT_GIB;
unit = UNIT_GIB;
} else if (bytesize % UNIT_MIB === 0) {
size = bytesize / UNIT_MIB;
unit = UNIT_MIB;
} else if (bytesize % UNIT_KIB === 0) {
size = bytesize / UNIT_KIB;
unit = UNIT_KIB;
}
return [size, unit];
};
const getPackageDescription = async (release, arch, repoUrls, packageName) => {
let pack;
// if the env is stage beta then use content-sources api
// else use image-builder api
if (insights.chrome.isBeta()) {
const data = await api.getPackagesContentSources(repoUrls, packageName);
pack = data.find((pack) => packageName === pack.name);
} else {
const args = [release, arch, packageName];
const response = await api.getPackages(...args);
let { data } = response;
const { meta } = response;
// the package should be found in the 0 index
// if not then fetch all package matches and search for the package
if (data[0]?.name === packageName) {
pack = data[0];
} else {
if (data?.length !== meta.count) {
({ data } = await api.getPackages(...args, meta.count));
}
pack = data.find((pack) => packageName === pack.name);
}
}
const summary = pack?.summary;
// if no matching package is found return an empty string for description
return summary || '';
};
// map the compose request object to the expected form state
const requestToState = (composeRequest) => {
if (composeRequest) {
const imageRequest = composeRequest.image_requests[0];
const uploadRequest = imageRequest.upload_request;
const formState = {};
formState['image-name'] = composeRequest.image_name;
formState.release = composeRequest?.distribution;
// set defaults for target environment first
formState['target-environment'] = {
aws: false,
azure: false,
gcp: false,
'guest-image': false,
};
// then select the one from the request
// if the image type is to a cloud provider we use the upload_request.type
// or if the image is intended for download we use the image_type
let targetEnvironment;
if (uploadRequest.type === 'aws.s3') {
targetEnvironment = imageRequest.image_type;
} else {
targetEnvironment = uploadRequest.type;
}
formState['target-environment'][targetEnvironment] = true;
if (targetEnvironment === 'aws') {
const shareWithSource = uploadRequest?.options?.share_with_sources?.[0];
const shareWithAccount = uploadRequest?.options?.share_with_accounts?.[0];
formState['aws-sources-select'] = shareWithSource;
formState['aws-account-id'] = shareWithAccount;
if (shareWithAccount && !shareWithSource) {
formState['aws-target-type'] = 'aws-target-type-account-id';
} else {
// if both shareWithAccount & shareWithSource are present, set radio
// to sources - this is essentially an arbitrary decision
// additionally, note that the source is not validated against the actual
// sources
formState['aws-target-type'] = 'aws-target-type-source';
}
} else if (targetEnvironment === 'azure') {
formState['azure-tenant-id'] = uploadRequest?.options?.tenant_id;
formState['azure-subscription-id'] =
uploadRequest?.options?.subscription_id;
formState['azure-resource-group'] =
uploadRequest?.options?.resource_group;
} else if (targetEnvironment === 'gcp') {
// parse google account info
// roughly in the format `accountType:accountEmail`
const accountInfo = uploadRequest?.options?.share_with_accounts[0];
const [accountTypePrefix, account] = accountInfo.split(':');
switch (accountTypePrefix) {
case 'user':
formState['google-account-type'] = 'googleAccount';
formState['google-email'] = account;
break;
case 'serviceAccount':
formState['google-account-type'] = 'serviceAccount';
formState['google-email'] = account;
break;
case 'group':
formState['google-account-type'] = 'googleGroup';
formState['google-email'] = account;
break;
case 'domain':
formState['google-account-type'] = 'domain';
formState['google-domain'] = account;
break;
}
}
// customizations
// packages
const packs = [];
const distro = composeRequest?.distribution;
const distroRepoUrls = getDistroRepoUrls(distro);
const payloadRepositories =
composeRequest?.customizations?.payload_repositories?.map(
(repo) => repo.baseurl
);
const repoUrls = [...distroRepoUrls];
payloadRepositories ? repoUrls.push(...payloadRepositories) : null;
composeRequest?.customizations?.packages?.forEach(async (packName) => {
const packageDescription = await getPackageDescription(
distro,
imageRequest?.architecture,
repoUrls,
packName
);
const pack = {
name: packName,
summary: packageDescription,
};
packs.push(pack);
});
formState['selected-packages'] = packs;
// repositories
// 'original-payload-repositories' is treated as read-only and is used to populate
// the table in the repositories step
// This is necessary because there may be repositories present in the request's
// json blob that are not managed using the content sources API. In that case,
// they are still displayed in the table of repositories but without any information
// from the content sources API (in other words, only the URL of the repository is
// displayed). This information needs to persist throughout the lifetime of the
// Wizard as it is needed every time the repositories step is visited.
formState['original-payload-repositories'] =
composeRequest?.customizations?.payload_repositories;
// 'custom-repositories' is mutable and is used to generate the request
// sent to image-builder
formState['custom-repositories'] =
composeRequest?.customizations?.payload_repositories;
// filesystem
const fs = composeRequest?.customizations?.filesystem;
if (fs) {
formState['file-system-config-radio'] = 'manual';
const fileSystemConfiguration = [];
for (const fsc of fs) {
const [size, unit] = parseSizeUnit(fsc.min_size);
fileSystemConfiguration.push({
mountpoint: fsc.mountpoint,
size,
unit,
});
}
formState['file-system-configuration'] = fileSystemConfiguration;
}
// subscription
const subscription = composeRequest?.customizations?.subscription;
if (subscription) {
if (subscription.insights) {
formState['register-system'] = 'register-now-insights';
} else {
formState['register-system'] = 'register-now';
}
formState['subscription-activation-key'] = subscription['activation-key'];
formState['subscription-organization-id'] = subscription.organization;
if (insights.chrome.isProd()) {
formState['subscription-server-url'] = 'subscription.rhsm.redhat.com';
formState['subscription-base-url'] = 'https://cdn.redhat.com/';
} else {
formState['subscription-server-url'] =
'subscription.rhsm.stage.redhat.com';
formState['subscription-base-url'] = 'https://cdn.stage.redhat.com/';
}
} else {
formState['register-system'] = 'register-later';
}
return formState;
} else {
return;
}
};
const formStepHistory = (composeRequest) => {
if (composeRequest) {
const imageRequest = composeRequest.image_requests[0];
const uploadRequest = imageRequest.upload_request;
// the order of steps must match the order of the steps in the Wizard
const steps = ['image-output'];
if (uploadRequest.type === 'aws') {
steps.push('aws-target-env');
} else if (uploadRequest.type === 'azure') {
steps.push('azure-target-env');
} else if (uploadRequest.type === 'gcp') {
steps.push('google-cloud-target-env');
}
if (isRhel(composeRequest?.distribution)) {
steps.push('registration');
}
if (insights.chrome.isBeta()) {
steps.push('File system configuration', 'packages', 'repositories');
const customRepositories =
composeRequest.customizations?.payload_repositories;
if (customRepositories) {
steps.push('packages-content-sources');
}
} else {
steps.push('File system configuration', 'packages');
}
steps.push('image-name');
return steps;
} else {
return [];
}
};
const CreateImageWizard = () => {
const awsTarget = insights.chrome.isBeta() ? awsTargetBeta : awsTargetStable;
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
const composeRequest = location?.state?.composeRequest;
const initialState = requestToState(composeRequest);
const stepHistory = formStepHistory(composeRequest);
const handleClose = () => navigate(resolveRelPath(''));
useEffect(() => {
if (insights.chrome.isBeta()) {
dispatch(fetchRepositories());
}
}, []);
return (
<ImageCreator
onClose={handleClose}
onSubmit={({ values, setIsSaving }) => {
setIsSaving(() => true);
const requests = onSave(values);
Promise.all(
requests.map((request) =>
api.composeImage(request).then((response) => {
dispatch(
composeAdded({
compose: {
...response,
request,
image_status: { status: 'pending' },
},
insert: true,
})
);
})
)
)
.then(() => {
navigate(resolveRelPath(''));
dispatch(
addNotification({
variant: 'success',
title: 'Your image is being created',
})
);
setIsSaving(false);
})
.catch((err) => {
dispatch(
addNotification({
variant: 'danger',
title: 'Your image could not be created',
description:
'Status code ' +
err.response.status +
': ' +
err.response.statusText,
})
);
setIsSaving(false);
});
}}
defaultArch="x86_64"
customValidatorMapper={{
fileSystemConfigurationValidator,
targetEnvironmentValidator,
}}
schema={{
fields: [
{
component: componentTypes.WIZARD,
name: 'image-builder-wizard',
className: 'imageBuilder',
isDynamic: true,
inModal: true,
onKeyDown: (e) => {
handleKeyDown(e, handleClose);
},
buttonLabels: {
submit: 'Create image',
},
showTitles: true,
title: 'Create image',
crossroads: [
'target-environment',
'release',
'custom-repositories',
],
description: (
<>
Image builder allows you to create a custom image and push it to
target environments. <DocumentationButton />
</>
),
// order in this array does not reflect order in wizard nav, this order is managed inside
// of each step by `nextStep` property!
fields: [
imageOutput,
awsTarget,
googleCloudTarger,
msAzureTarget,
registration,
packages,
packagesContentSources,
repositories,
fileSystemConfiguration,
imageName,
review,
],
initialState: {
activeStep: location?.state?.initialStep || 'image-output', // name of the active step
activeStepIndex: stepHistory.length, // active index
maxStepIndex: stepHistory.length, // max achieved index
prevSteps: stepHistory, // array with names of previously visited steps
},
},
],
}}
initialValues={initialState}
/>
);
};
export default CreateImageWizard;