wizard: allow the creation of aarch64 images

This commit extends the supported architectures to aarch64. In the image
output step (the first one of the wizard) the user is now faced with a
new select choices to pickup the architecture they want to build.

Now the set of compatible targets to build is dynamically loaded from
the backend and the UX changes what's accessible on the fly depending on
what's the user has been selected.

Refs HMS-1135
This commit is contained in:
Thomas Lavocat 2023-10-13 16:46:03 +02:00 committed by jkozol
parent d91727bf38
commit 938340b360
8 changed files with 374 additions and 199 deletions

View file

@ -112,7 +112,7 @@ const onSave = (values) => {
image_description: values?.['image-description'],
image_requests: [
{
architecture: 'x86_64',
architecture: values['arch'],
image_type: 'aws',
upload_request: {
type: 'aws',
@ -151,7 +151,7 @@ const onSave = (values) => {
image_description: values?.['image-description'],
image_requests: [
{
architecture: 'x86_64',
architecture: values['arch'],
image_type: 'gcp',
upload_request: {
type: 'gcp',
@ -182,7 +182,7 @@ const onSave = (values) => {
image_description: values?.['image-description'],
image_requests: [
{
architecture: 'x86_64',
architecture: values['arch'],
image_type: 'azure',
upload_request: {
type: 'azure',
@ -205,7 +205,7 @@ const onSave = (values) => {
image_description: values?.['image-description'],
image_requests: [
{
architecture: 'x86_64',
architecture: values['arch'],
image_type: 'vsphere',
upload_request: {
type: 'aws.s3',
@ -225,7 +225,7 @@ const onSave = (values) => {
image_description: values?.['image-description'],
image_requests: [
{
architecture: 'x86_64',
architecture: values['arch'],
image_type: 'vsphere-ova',
upload_request: {
type: 'aws.s3',
@ -245,7 +245,7 @@ const onSave = (values) => {
image_description: values?.['image-description'],
image_requests: [
{
architecture: 'x86_64',
architecture: values['arch'],
image_type: 'guest-image',
upload_request: {
type: 'aws.s3',
@ -265,7 +265,7 @@ const onSave = (values) => {
image_description: values?.['image-description'],
image_requests: [
{
architecture: 'x86_64',
architecture: values['arch'],
image_type: 'image-installer',
upload_request: {
type: 'aws.s3',
@ -285,7 +285,7 @@ const onSave = (values) => {
image_description: values?.['image-description'],
image_requests: [
{
architecture: 'x86_64',
architecture: values['arch'],
image_type: 'wsl',
upload_request: {
type: 'aws.s3',
@ -330,6 +330,7 @@ const requestToState = (composeRequest, distroInfo, isProd, enableOscap) => {
formState['image-description'] = composeRequest.image_description;
formState.release = composeRequest?.distribution;
formState.arch = imageRequest.architecture;
// set defaults for target environment first
formState['target-environment'] = {
aws: false,

View file

@ -31,6 +31,7 @@
.tile {
flex: 1 0 0px;
max-width: 250px;
}
.pf-c-tile:focus {

View file

@ -8,6 +8,7 @@ import { Spinner } from '@patternfly/react-core';
import PropTypes from 'prop-types';
import ActivationKeys from './formComponents/ActivationKeys';
import ArchSelect from './formComponents/ArchSelect';
import { AWSSourcesSelect } from './formComponents/AWSSourcesSelect';
import AzureAuthButton from './formComponents/AzureAuthButton';
import AzureResourceGroups from './formComponents/AzureResourceGroups';
@ -74,6 +75,7 @@ const ImageCreator = ({
'azure-resource-groups': AzureResourceGroups,
'gallery-layout': GalleryLayout,
'oscap-profile-selector': Oscap,
'image-output-arch-select': ArchSelect,
registration: Registration,
...customComponentMapper,
}}

View file

@ -0,0 +1,64 @@
import React, { useState } from 'react';
import { FormSpy } from '@data-driven-forms/react-form-renderer';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import { FormGroup } from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import PropTypes from 'prop-types';
import { ARCHS } from '../../../constants';
const ArchSelect = ({ label, isRequired, ...props }) => {
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [isOpen, setIsOpen] = useState(false);
const setArch = (_, selection) => {
change(input.name, selection);
setIsOpen(false);
};
const setSelectOptions = () => {
var options = [];
ARCHS.forEach((arch) => {
options.push(
<SelectOption key={arch} value={arch}>
{arch}
</SelectOption>
);
});
return options;
};
return (
<FormSpy>
{() => (
<FormGroup isRequired={isRequired} label={label}>
<Select
ouiaId="arch_select"
variant={SelectVariant.single}
onToggle={() => setIsOpen(!isOpen)}
onSelect={setArch}
selections={getState()?.values?.[input.name]}
isOpen={isOpen}
>
{setSelectOptions()}
</Select>
</FormGroup>
)}
</FormSpy>
);
};
ArchSelect.propTypes = {
label: PropTypes.node,
isRequired: PropTypes.bool,
};
export default ArchSelect;

View file

@ -25,7 +25,7 @@ import {
RepositoriesTable,
} from './ReviewStepTables';
import { RELEASES, UNIT_GIB } from '../../../constants';
import { ARCHS, RELEASES, UNIT_GIB } from '../../../constants';
import { extractProvisioningList } from '../../../store/helpers';
import { useGetSourceListQuery } from '../../../store/provisioningApi';
import { useShowActivationKeyQuery } from '../../../store/rhsmApi';
@ -57,7 +57,9 @@ export const ImageOutputList = () => {
<TextListItem component={TextListItemVariants.dt}>
Architecture
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>x86_64</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{getState()?.values?.arch}
</TextListItem>
</TextList>
<br />
</TextContent>

View file

@ -3,10 +3,13 @@ import React, { useEffect, useState } from 'react';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import {
Alert,
Bullseye,
Checkbox,
FormGroup,
Popover,
Radio,
Spinner,
Text,
TextContent,
TextVariants,
@ -14,11 +17,34 @@ import {
} from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';
import PropTypes from 'prop-types';
import { useField } from 'react-final-form';
import { RHEL_8, CENTOS_8 } from '../../../constants.js';
import { useGetArchitecturesQuery } from '../../../store/imageBuilderApi';
import { provisioningApi } from '../../../store/provisioningApi';
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
const useGetAllowedTargets = ({ architecture, release }) => {
const { data, isFetching, isSuccess, isError } = useGetArchitecturesQuery({
distribution: release,
});
let image_types = [];
if (isSuccess && data) {
data.forEach((elem) => {
if (elem.arch === architecture) {
image_types = elem.image_types;
}
});
}
return {
data: image_types,
isFetching: isFetching,
isSuccess: isSuccess,
isError: isError,
};
};
const TargetEnvironment = ({ label, isRequired, ...props }) => {
const { getState, change } = useFormApi();
const { input } = useFieldApi({ label, isRequired, ...props });
@ -52,17 +78,67 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
return newEnv;
});
// hack, wsl isn't supported in el9, causes a rerender
if (environment.wsl && ![RHEL_8, CENTOS_8].includes(release)) {
handleSetEnvironment('wsl', false);
}
const handleKeyDown = (e, env, checked) => {
if (e.key === ' ') {
handleSetEnvironment(env, checked);
}
};
// Load all the allowed targets from the backend
const architecture = useField('arch').input.value;
const {
data: allowedTargets,
isFetching,
isSuccess,
isError,
} = useGetAllowedTargets({
architecture: architecture,
release: release,
});
if (isFetching) {
return (
<Bullseye>
<Spinner size="lg" />
</Bullseye>
);
}
if (isError || !isSuccess) {
return (
<Alert
variant={'danger'}
isPlain
isInline
title={'Allowed targets unavailable'}
>
Allowed targets cannot be reached, try again later.
</Alert>
);
}
// If the user already made a choice for some targets but then changes their
// architecture or distribution, only keep the target choices that are still
// compatible.
const allTargets = [
'aws',
'gcp',
'azure',
'vsphere',
'vsphere-ova',
'guest-image',
'image-installer',
'wsl',
];
allTargets.forEach((target) => {
if (environment[target] && !allowedTargets.includes(target)) {
handleSetEnvironment(target, false);
}
});
// each item the user can select is depending on what's compatible with the
// architecture and the distribution they previously selected
return (
<FormGroup
isRequired={isRequired}
@ -74,195 +150,208 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
data-testid="target-public"
>
<div className="tiles">
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-aws"
title="Amazon Web Services"
icon={
<img
className="provider-icon"
src={'/apps/frontend-assets/partners-icons/aws.svg'}
alt="Amazon Web Services logo"
/>
}
onClick={() => handleSetEnvironment('aws', !environment.aws)}
onKeyDown={(e) => handleKeyDown(e, 'aws', !environment.aws)}
onMouseEnter={() => prefetchSources({ provider: 'aws' })}
isSelected={environment.aws}
isStacked
isDisplayLarge
/>
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-google"
title="Google Cloud Platform"
icon={
<img
className="provider-icon"
src={
'/apps/frontend-assets/partners-icons/google-cloud-short.svg'
}
alt="Google Cloud Platform logo"
/>
}
onClick={() => handleSetEnvironment('gcp', !environment.gcp)}
isSelected={environment.gcp}
onKeyDown={(e) => handleKeyDown(e, 'gcp', !environment.gcp)}
onMouseEnter={() => prefetchSources({ provider: 'gcp' })}
isStacked
isDisplayLarge
/>
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-azure"
title="Microsoft Azure"
icon={
<img
className="provider-icon"
src={
'/apps/frontend-assets/partners-icons/microsoft-azure-short.svg'
}
alt="Microsoft Azure logo"
/>
}
onClick={() => handleSetEnvironment('azure', !environment.azure)}
onKeyDown={(e) => handleKeyDown(e, 'azure', !environment.azure)}
onMouseEnter={() => prefetchSources({ provider: 'azure' })}
isSelected={environment.azure}
isStacked
isDisplayLarge
/>
{allowedTargets.includes('aws') && (
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-aws"
title="Amazon Web Services"
icon={
<img
className="provider-icon"
src={'/apps/frontend-assets/partners-icons/aws.svg'}
alt="Amazon Web Services logo"
/>
}
onClick={() => handleSetEnvironment('aws', !environment.aws)}
onKeyDown={(e) => handleKeyDown(e, 'aws', !environment.aws)}
onMouseEnter={() => prefetchSources({ provider: 'aws' })}
isSelected={environment.aws}
isStacked
isDisplayLarge
/>
)}
{allowedTargets.includes('gcp') && (
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-google"
title="Google Cloud Platform"
icon={
<img
className="provider-icon"
src={
'/apps/frontend-assets/partners-icons/google-cloud-short.svg'
}
alt="Google Cloud Platform logo"
/>
}
onClick={() => handleSetEnvironment('gcp', !environment.gcp)}
isSelected={environment.gcp}
onKeyDown={(e) => handleKeyDown(e, 'gcp', !environment.gcp)}
onMouseEnter={() => prefetchSources({ provider: 'gcp' })}
isStacked
isDisplayLarge
/>
)}
{allowedTargets.includes('azure') && (
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-azure"
title="Microsoft Azure"
icon={
<img
className="provider-icon"
src={
'/apps/frontend-assets/partners-icons/microsoft-azure-short.svg'
}
alt="Microsoft Azure logo"
/>
}
onClick={() => handleSetEnvironment('azure', !environment.azure)}
onKeyDown={(e) => handleKeyDown(e, 'azure', !environment.azure)}
onMouseEnter={() => prefetchSources({ provider: 'azure' })}
isSelected={environment.azure}
isStacked
isDisplayLarge
/>
)}
</div>
</FormGroup>
<FormGroup
label={<Text component={TextVariants.small}>Private cloud</Text>}
className="pf-u-mt-sm"
data-testid="target-private"
>
<Checkbox
label="VMWare vSphere"
isChecked={environment.vsphere || environment['vsphere-ova']}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', checked);
handleSetEnvironment('vsphere', false);
}}
aria-label="VMWare checkbox"
id="checkbox-vmware"
name="VMWare"
data-testid="checkbox-vmware"
/>
</FormGroup>
<FormGroup
className="pf-u-mt-sm pf-u-mb-sm pf-u-ml-xl"
data-testid="target-private-vsphere-radio"
>
<Radio
name="vsphere-radio"
aria-label="VMWare vSphere radio button OVA"
id="vsphere-radio-ova"
label={
<>
Open virtualization format (.ova)
<Popover
maxWidth="30rem"
position="right"
bodyContent={
<TextContent>
<Text>
An OVA file is a virtual appliance used by virtualization
platforms such as VMWare vSphere. It is a package that
contains files used to describe a virtual machine, which
includes a VMDK image, OVF descriptor file and a manifest
file.
</Text>
</TextContent>
}
>
<HelpIcon className="pf-u-ml-sm" />
</Popover>
</>
}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', checked);
handleSetEnvironment('vsphere', !checked);
}}
isChecked={environment['vsphere-ova']}
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
/>
<Radio
{allowedTargets.includes('vsphere') && (
<FormGroup
label={<Text component={TextVariants.small}>Private cloud</Text>}
className="pf-u-mt-sm"
name="vsphere-radio"
aria-label="VMWare vSphere radio button VMDK"
id="vsphere-radio-vmdk"
label={
<>
Virtual disk (.vmdk)
<Popover
maxWidth="30rem"
position="right"
bodyContent={
<TextContent>
<Text>
A VMDK file is a virtual disk that stores the contents of
a virtual machine. This disk has to be imported into
vSphere using govc import.vmdk, use the OVA version when
using the vSphere UI.
</Text>
</TextContent>
}
>
<HelpIcon className="pf-u-ml-sm" />
</Popover>
</>
}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', !checked);
handleSetEnvironment('vsphere', checked);
}}
isChecked={environment.vsphere}
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
/>
</FormGroup>
data-testid="target-private"
>
<Checkbox
label="VMWare vSphere"
isChecked={environment.vsphere || environment['vsphere-ova']}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', checked);
handleSetEnvironment('vsphere', false);
}}
aria-label="VMWare checkbox"
id="checkbox-vmware"
name="VMWare"
data-testid="checkbox-vmware"
/>
</FormGroup>
)}
{allowedTargets.includes('vsphere') && (
<FormGroup
className="pf-u-mt-sm pf-u-mb-sm pf-u-ml-xl"
data-testid="target-private-vsphere-radio"
>
{allowedTargets.includes('vsphere-ova') && (
<Radio
name="vsphere-radio"
aria-label="VMWare vSphere radio button OVA"
id="vsphere-radio-ova"
label={
<>
Open virtualization format (.ova)
<Popover
maxWidth="30rem"
position="right"
bodyContent={
<TextContent>
<Text>
An OVA file is a virtual appliance used by
virtualization platforms such as VMWare vSphere. It is
a package that contains files used to describe a
virtual machine, which includes a VMDK image, OVF
descriptor file and a manifest file.
</Text>
</TextContent>
}
>
<HelpIcon className="pf-u-ml-sm" />
</Popover>
</>
}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', checked);
handleSetEnvironment('vsphere', !checked);
}}
isChecked={environment['vsphere-ova']}
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
/>
)}
<Radio
className="pf-u-mt-sm"
name="vsphere-radio"
aria-label="VMWare vSphere radio button VMDK"
id="vsphere-radio-vmdk"
label={
<>
Virtual disk (.vmdk)
<Popover
maxWidth="30rem"
position="right"
bodyContent={
<TextContent>
<Text>
A VMDK file is a virtual disk that stores the contents
of a virtual machine. This disk has to be imported into
vSphere using govc import.vmdk, use the OVA version when
using the vSphere UI.
</Text>
</TextContent>
}
>
<HelpIcon className="pf-u-ml-sm" />
</Popover>
</>
}
onChange={(_event, checked) => {
handleSetEnvironment('vsphere-ova', !checked);
handleSetEnvironment('vsphere', checked);
}}
isChecked={environment.vsphere}
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
/>
</FormGroup>
)}
<FormGroup
label={<Text component={TextVariants.small}>Other</Text>}
data-testid="target-other"
>
<Checkbox
label="Virtualization - Guest image (.qcow2)"
isChecked={environment['guest-image']}
onChange={(_event, checked) =>
handleSetEnvironment('guest-image', checked)
}
aria-label="Virtualization guest image checkbox"
id="checkbox-guest-image"
name="Virtualization guest image"
data-testid="checkbox-guest-image"
/>
<Checkbox
label="Bare metal - Installer (.iso)"
isChecked={environment['image-installer']}
onChange={(_event, checked) =>
handleSetEnvironment('image-installer', checked)
}
aria-label="Bare metal installer checkbox"
id="checkbox-image-installer"
name="Bare metal installer"
data-testid="checkbox-image-installer"
/>
{[RHEL_8, CENTOS_8].includes(getState()?.values?.release) &&
isBeta() && (
<Checkbox
label="WSL - Windows Subsystem for Linux (.tar.gz)"
isChecked={environment['wsl']}
onChange={(_event, checked) =>
handleSetEnvironment('wsl', checked)
}
aria-label="windows subsystem for linux checkbox"
id="checkbox-wsl"
name="WSL"
data-testid="checkbox-wsl"
/>
)}
{allowedTargets.includes('guest-image') && (
<Checkbox
label="Virtualization - Guest image (.qcow2)"
isChecked={environment['guest-image']}
onChange={(_event, checked) =>
handleSetEnvironment('guest-image', checked)
}
aria-label="Virtualization guest image checkbox"
id="checkbox-guest-image"
name="Virtualization guest image"
data-testid="checkbox-guest-image"
/>
)}
{allowedTargets.includes('image-installer') && (
<Checkbox
label="Bare metal - Installer (.iso)"
isChecked={environment['image-installer']}
onChange={(_event, checked) =>
handleSetEnvironment('image-installer', checked)
}
aria-label="Bare metal installer checkbox"
id="checkbox-image-installer"
name="Bare metal installer"
data-testid="checkbox-image-installer"
/>
)}
{allowedTargets.includes('wsl') && isBeta && (
<Checkbox
label="WSL - Windows Subsystem for Linux (.tar.gz)"
isChecked={environment['wsl']}
onChange={(_event, checked) => handleSetEnvironment('wsl', checked)}
aria-label="windows subsystem for linux checkbox"
id="checkbox-wsl"
name="WSL"
data-testid="checkbox-wsl"
/>
)}
</FormGroup>
</FormGroup>
);

View file

@ -7,7 +7,7 @@ import { Text } from '@patternfly/react-core';
import nextStepMapper from './imageOutputStepMapper';
import StepTemplate from './stepTemplate';
import { RHEL_9 } from '../../../constants.js';
import { RHEL_9, X86_64 } from '../../../constants.js';
import DocumentationButton from '../../sharedComponents/DocumentationButton';
import CustomButtons from '../formComponents/CustomButtons';
@ -43,6 +43,18 @@ const imageOutputStep = {
},
],
},
{
component: 'image-output-arch-select',
label: 'Architecture',
name: 'arch',
initialValue: X86_64,
isRequired: true,
validate: [
{
type: validatorTypes.REQUIRED,
},
],
},
{
component: 'centos-acknowledgement',
name: 'centos-acknowledgement',

View file

@ -7,6 +7,8 @@ export const RHEL_8 = 'rhel-88';
export const RHEL_9 = 'rhel-92';
export const CENTOS_8 = 'centos-8';
export const CENTOS_9 = 'centos-9';
export const X86_64 = 'x86_64';
export const AARCH64 = 'aarch64';
export const UNIT_KIB = 1024 ** 1;
export const UNIT_MIB = 1024 ** 2;
@ -19,6 +21,8 @@ export const RELEASES = new Map([
[CENTOS_8, 'CentOS Stream 8'],
]);
export const ARCHS = [X86_64, AARCH64];
export const DEFAULT_AWS_REGION = 'us-east-1';
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html