Removing the server store makes the way we handle data going in and out of the wizard state more consistent. Each customisation is mapped into the wizard state and pulled out when generating the blueprint payload. When the services and kernel customisations are implemented, this information will need to be stored inside of the wizard state anyway. Lastly this will make implementing a compliance step easier for edit mode, removing the need to write to the wizard state from within the server store when only a compliance policy id is available (on the review page), which would be used to fetch the profile ref id, which would in turn be used to fetch the customisations not stored in the wizard state.
607 lines
18 KiB
TypeScript
607 lines
18 KiB
TypeScript
import { Store } from 'redux';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
import { parseSizeUnit } from './parseSizeUnit';
|
|
|
|
import {
|
|
CENTOS_9,
|
|
FIRST_BOOT_SERVICE,
|
|
FIRST_BOOT_SERVICE_DATA,
|
|
RHEL_8,
|
|
RHEL_9,
|
|
} from '../../../constants';
|
|
import { RootState } from '../../../store';
|
|
import {
|
|
AwsUploadRequestOptions,
|
|
AzureUploadRequestOptions,
|
|
BlueprintExportResponse,
|
|
BlueprintResponse,
|
|
CreateBlueprintRequest,
|
|
Customizations,
|
|
DistributionProfileItem,
|
|
Distributions,
|
|
File,
|
|
Filesystem,
|
|
GcpUploadRequestOptions,
|
|
ImageRequest,
|
|
ImageTypes,
|
|
OpenScap,
|
|
Services,
|
|
Subscription,
|
|
UploadTypes,
|
|
} from '../../../store/imageBuilderApi';
|
|
import {
|
|
selectActivationKey,
|
|
selectArchitecture,
|
|
selectAwsAccountId,
|
|
selectAwsShareMethod,
|
|
selectAwsSourceId,
|
|
selectAzureResourceGroup,
|
|
selectAzureShareMethod,
|
|
selectAzureSource,
|
|
selectAzureSubscriptionId,
|
|
selectAzureTenantId,
|
|
selectBaseUrl,
|
|
selectBlueprintDescription,
|
|
selectBlueprintName,
|
|
selectCustomRepositories,
|
|
selectDistribution,
|
|
selectGcpAccountType,
|
|
selectGcpEmail,
|
|
selectGcpShareMethod,
|
|
selectGroups,
|
|
selectImageTypes,
|
|
selectKernel,
|
|
selectPackages,
|
|
selectPayloadRepositories,
|
|
selectRecommendedRepositories,
|
|
selectProfile,
|
|
selectRegistrationType,
|
|
selectServerUrl,
|
|
selectServices,
|
|
wizardState,
|
|
selectFileSystemConfigurationType,
|
|
selectPartitions,
|
|
selectSnapshotDate,
|
|
selectUseLatest,
|
|
selectFirstBootScript,
|
|
selectMetadata,
|
|
initialState,
|
|
} from '../../../store/wizardSlice';
|
|
import {
|
|
convertMMDDYYYYToYYYYMMDD,
|
|
convertYYYYMMDDTOMMDDYYYY,
|
|
} from '../../../Utilities/time';
|
|
import { FileSystemConfigurationType } from '../steps/FileSystem';
|
|
import {
|
|
getConversionFactor,
|
|
Partition,
|
|
Units,
|
|
} from '../steps/FileSystem/FileSystemConfiguration';
|
|
import { PackageRepository } from '../steps/Packages/Packages';
|
|
import {
|
|
convertSchemaToIBCustomRepo,
|
|
convertSchemaToIBPayloadRepo,
|
|
} from '../steps/Repositories/components/Utilities';
|
|
import { AwsShareMethod } from '../steps/TargetEnvironment/Aws';
|
|
import { AzureShareMethod } from '../steps/TargetEnvironment/Azure';
|
|
import { GcpAccountType, GcpShareMethod } from '../steps/TargetEnvironment/Gcp';
|
|
|
|
/**
|
|
* This function maps the wizard state to a valid CreateBlueprint request object
|
|
* @param {Store} store redux store
|
|
* @param {string} orgID organization ID
|
|
*
|
|
* @returns {CreateBlueprintRequest} blueprint creation request payload
|
|
*/
|
|
export const mapRequestFromState = (
|
|
store: Store,
|
|
orgID: string
|
|
): CreateBlueprintRequest => {
|
|
const state = store.getState();
|
|
const imageRequests = getImageRequests(state);
|
|
const customizations = getCustomizations(state, orgID);
|
|
|
|
return {
|
|
name: selectBlueprintName(state),
|
|
metadata: selectMetadata(state),
|
|
description: selectBlueprintDescription(state),
|
|
distribution: selectDistribution(state),
|
|
image_requests: imageRequests,
|
|
customizations,
|
|
};
|
|
};
|
|
|
|
const convertFilesystemToPartition = (filesystem: Filesystem): Partition => {
|
|
const id = uuidv4();
|
|
const [size, unit] = parseSizeUnit(filesystem.min_size);
|
|
const partition = {
|
|
mountpoint: filesystem.mountpoint,
|
|
min_size: size,
|
|
id: id,
|
|
unit: unit as Units,
|
|
};
|
|
return partition;
|
|
};
|
|
|
|
/**
|
|
* This function overwrites distribution of the blueprints with the major release
|
|
* and deprecated CentOS 8 with CentOS 9
|
|
* Minor releases were previously used and are still present in older blueprints
|
|
* @param distribution blueprint distribution
|
|
*/
|
|
const getLatestRelease = (distribution: Distributions) => {
|
|
return distribution.startsWith('rhel-9')
|
|
? RHEL_9
|
|
: distribution.startsWith('rhel-8')
|
|
? RHEL_8
|
|
: distribution === ('centos-8' as Distributions)
|
|
? CENTOS_9
|
|
: distribution;
|
|
};
|
|
|
|
function commonRequestToState(
|
|
request: BlueprintResponse | CreateBlueprintRequest
|
|
) {
|
|
const gcp = request.image_requests.find(
|
|
(image) => image.image_type === 'gcp'
|
|
);
|
|
const aws = request.image_requests.find(
|
|
(image) => image.image_type === 'aws'
|
|
);
|
|
|
|
const azure = request.image_requests.find(
|
|
(image) => image.image_type === 'azure'
|
|
);
|
|
|
|
const snapshot_date = convertYYYYMMDDTOMMDDYYYY(
|
|
request.image_requests.find((image) => !!image.snapshot_date)
|
|
?.snapshot_date || ''
|
|
);
|
|
|
|
const awsUploadOptions = aws?.upload_request
|
|
.options as AwsUploadRequestOptions;
|
|
const gcpUploadOptions = gcp?.upload_request
|
|
.options as GcpUploadRequestOptions;
|
|
const azureUploadOptions = azure?.upload_request
|
|
.options as AzureUploadRequestOptions;
|
|
|
|
const arch =
|
|
request.image_requests[0]?.architecture || initialState.architecture;
|
|
if (arch !== 'x86_64' && arch !== 'aarch64') {
|
|
throw new Error(`image type: ${arch} has no implementation yet`);
|
|
}
|
|
return {
|
|
details: {
|
|
blueprintName: request.name || '',
|
|
blueprintDescription: request.description || '',
|
|
},
|
|
openScap: request.customizations
|
|
? {
|
|
profile: request.customizations.openscap
|
|
?.profile_id as DistributionProfileItem,
|
|
}
|
|
: initialState.openScap,
|
|
firstBoot: request.customizations
|
|
? {
|
|
script: getFirstBootScript(request.customizations.files),
|
|
}
|
|
: initialState.firstBoot,
|
|
fileSystem: request.customizations?.filesystem
|
|
? {
|
|
mode: 'manual' as FileSystemConfigurationType,
|
|
partitions: request.customizations?.filesystem.map((fs) =>
|
|
convertFilesystemToPartition(fs)
|
|
),
|
|
}
|
|
: {
|
|
mode: 'automatic' as FileSystemConfigurationType,
|
|
partitions: [],
|
|
},
|
|
architecture: arch,
|
|
distribution:
|
|
getLatestRelease(request.distribution) || initialState.distribution,
|
|
imageTypes: request.image_requests.map((image) => image.image_type),
|
|
azure: {
|
|
shareMethod: (azureUploadOptions?.source_id
|
|
? 'sources'
|
|
: 'manual') as AzureShareMethod,
|
|
source: azureUploadOptions?.source_id || '',
|
|
tenantId: azureUploadOptions?.tenant_id || '',
|
|
subscriptionId: azureUploadOptions?.subscription_id || '',
|
|
resourceGroup: azureUploadOptions?.resource_group,
|
|
},
|
|
gcp: {
|
|
shareMethod: (gcpUploadOptions?.share_with_accounts
|
|
? 'withGoogle'
|
|
: 'withInsights') as GcpShareMethod,
|
|
accountType: gcpUploadOptions?.share_with_accounts?.[0].split(
|
|
':'
|
|
)[0] as GcpAccountType,
|
|
email: gcpUploadOptions?.share_with_accounts?.[0].split(':')[1] || '',
|
|
},
|
|
aws: {
|
|
accountId: awsUploadOptions?.share_with_accounts?.[0] || '',
|
|
shareMethod: (awsUploadOptions?.share_with_sources
|
|
? 'sources'
|
|
: 'manual') as AwsShareMethod,
|
|
source: { id: awsUploadOptions?.share_with_sources?.[0] },
|
|
sourceId: awsUploadOptions?.share_with_sources?.[0],
|
|
},
|
|
snapshotting: {
|
|
useLatest: !snapshot_date,
|
|
snapshotDate: snapshot_date,
|
|
},
|
|
repositories: {
|
|
customRepositories: request.customizations?.custom_repositories || [],
|
|
payloadRepositories: request.customizations?.payload_repositories || [],
|
|
recommendedRepositories: [],
|
|
},
|
|
packages:
|
|
request.customizations?.packages
|
|
?.filter((pkg) => !pkg.startsWith('@'))
|
|
.map((pkg) => ({
|
|
name: pkg,
|
|
summary: '',
|
|
repository: '' as PackageRepository,
|
|
})) || [],
|
|
groups:
|
|
request.customizations?.packages
|
|
?.filter((grp) => grp.startsWith('@'))
|
|
.map((grp) => ({
|
|
name: grp.substr(1),
|
|
description: '',
|
|
repository: '' as PackageRepository,
|
|
package_list: [],
|
|
})) || [],
|
|
services: {
|
|
enabled: request.customizations?.services?.enabled || [],
|
|
masked: request.customizations?.services?.masked || [],
|
|
disabled: request.customizations?.services?.disabled || [],
|
|
},
|
|
kernel: {
|
|
append: request.customizations?.kernel?.append || '',
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This function maps the blueprint response to the wizard state, used to populate the wizard with the blueprint details
|
|
* @param request BlueprintResponse
|
|
* @param source V1ListSourceResponseItem
|
|
* @returns wizardState
|
|
*/
|
|
export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
|
const wizardMode = 'edit';
|
|
return {
|
|
wizardMode,
|
|
blueprintId: request.id,
|
|
env: {
|
|
serverUrl: request.customizations.subscription?.['server-url'] || '',
|
|
baseUrl: request.customizations.subscription?.['base-url'] || '',
|
|
},
|
|
registration: {
|
|
registrationType: request.customizations?.subscription
|
|
? request.customizations.subscription.rhc
|
|
? 'register-now-rhc'
|
|
: 'register-now-insights'
|
|
: 'register-later',
|
|
activationKey: request.customizations.subscription?.['activation-key'],
|
|
},
|
|
...commonRequestToState(request),
|
|
};
|
|
};
|
|
|
|
/**
|
|
* This function maps the blueprint response to the wizard state, used to populate the wizard with the blueprint details
|
|
* @param request BlueprintExportResponse
|
|
* @returns wizardState
|
|
*/
|
|
export const mapExportRequestToState = (
|
|
request: BlueprintExportResponse,
|
|
image_requests: ImageRequest[]
|
|
): wizardState => {
|
|
const wizardMode = 'create';
|
|
const blueprintResponse: CreateBlueprintRequest = {
|
|
name: request.name,
|
|
description: request.description,
|
|
distribution: request.distribution,
|
|
customizations: request.customizations,
|
|
image_requests: image_requests,
|
|
};
|
|
return {
|
|
wizardMode,
|
|
metadata: {
|
|
parent_id: request.metadata?.parent_id || null,
|
|
exported_at: request.metadata?.exported_at || '',
|
|
},
|
|
env: initialState.env,
|
|
registration: initialState.registration,
|
|
...commonRequestToState(blueprintResponse),
|
|
};
|
|
};
|
|
|
|
const getFirstBootScript = (files?: File[]): string => {
|
|
const firstBootFile = files?.find(
|
|
(file) => file.path === '/usr/local/sbin/custom-first-boot'
|
|
);
|
|
return firstBootFile?.data ? atob(firstBootFile.data) : '';
|
|
};
|
|
|
|
const getImageRequests = (state: RootState): ImageRequest[] => {
|
|
const imageTypes = selectImageTypes(state);
|
|
const snapshotDate = convertMMDDYYYYToYYYYMMDD(selectSnapshotDate(state));
|
|
const useLatest = selectUseLatest(state);
|
|
return imageTypes.map((type) => ({
|
|
architecture: selectArchitecture(state),
|
|
image_type: type,
|
|
upload_request: {
|
|
type: uploadTypeByTargetEnv(type),
|
|
options: getImageOptions(type, state),
|
|
},
|
|
snapshot_date: useLatest ? undefined : snapshotDate,
|
|
}));
|
|
};
|
|
|
|
const uploadTypeByTargetEnv = (imageType: ImageTypes): UploadTypes => {
|
|
switch (imageType) {
|
|
case 'aws':
|
|
return 'aws';
|
|
case 'gcp':
|
|
return 'gcp';
|
|
case 'azure':
|
|
return 'azure';
|
|
case 'oci':
|
|
return 'oci.objectstorage';
|
|
case 'wsl':
|
|
return 'aws.s3';
|
|
case 'guest-image':
|
|
return 'aws.s3';
|
|
case 'image-installer':
|
|
return 'aws.s3';
|
|
case 'vsphere':
|
|
return 'aws.s3';
|
|
case 'vsphere-ova':
|
|
return 'aws.s3';
|
|
case 'ami':
|
|
return 'aws';
|
|
default: {
|
|
// TODO: add edge type
|
|
throw new Error(`image type: ${imageType} has no implementation yet`);
|
|
}
|
|
}
|
|
};
|
|
const getImageOptions = (
|
|
imageType: ImageTypes,
|
|
state: RootState
|
|
):
|
|
| AwsUploadRequestOptions
|
|
| AzureUploadRequestOptions
|
|
| GcpUploadRequestOptions => {
|
|
switch (imageType) {
|
|
case 'aws':
|
|
if (selectAwsShareMethod(state) === 'sources')
|
|
return { share_with_sources: [selectAwsSourceId(state) || ''] };
|
|
else return { share_with_accounts: [selectAwsAccountId(state)] };
|
|
case 'azure':
|
|
if (selectAzureShareMethod(state) === 'sources')
|
|
return {
|
|
source_id: selectAzureSource(state),
|
|
resource_group: selectAzureResourceGroup(state),
|
|
};
|
|
else
|
|
return {
|
|
tenant_id: selectAzureTenantId(state),
|
|
subscription_id: selectAzureSubscriptionId(state),
|
|
resource_group: selectAzureResourceGroup(state),
|
|
};
|
|
case 'gcp': {
|
|
let googleAccount: string = '';
|
|
if (selectGcpShareMethod(state) === 'withGoogle') {
|
|
const gcpEmail = selectGcpEmail(state);
|
|
switch (selectGcpAccountType(state)) {
|
|
case 'user':
|
|
googleAccount = `user:${gcpEmail}`;
|
|
break;
|
|
case 'serviceAccount':
|
|
googleAccount = `serviceAccount:${gcpEmail}`;
|
|
break;
|
|
case 'group':
|
|
googleAccount = `group:${gcpEmail}`;
|
|
break;
|
|
case 'domain':
|
|
googleAccount = `domain:${gcpEmail}`;
|
|
}
|
|
return { share_with_accounts: [googleAccount] };
|
|
} else {
|
|
// TODO: GCP withInsights is not implemented yet
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
return {};
|
|
};
|
|
|
|
const getCustomizations = (state: RootState, orgID: string): Customizations => {
|
|
return {
|
|
containers: undefined,
|
|
directories: undefined,
|
|
files: selectFirstBootScript(state)
|
|
? [
|
|
{
|
|
path: '/etc/systemd/system/custom-first-boot.service',
|
|
data: FIRST_BOOT_SERVICE_DATA,
|
|
data_encoding: 'base64',
|
|
ensure_parents: true,
|
|
},
|
|
{
|
|
path: '/usr/local/sbin/custom-first-boot',
|
|
data: btoa(selectFirstBootScript(state)),
|
|
data_encoding: 'base64',
|
|
mode: '0774',
|
|
ensure_parents: true,
|
|
},
|
|
]
|
|
: undefined,
|
|
subscription: getSubscription(state, orgID),
|
|
packages: getPackages(state),
|
|
payload_repositories: getPayloadRepositories(state),
|
|
custom_repositories: getCustomRepositories(state),
|
|
openscap: getOpenscapProfile(state),
|
|
filesystem: getFileSystem(state),
|
|
users: undefined,
|
|
services: getServices(state),
|
|
hostname: undefined,
|
|
kernel: selectKernel(state).append
|
|
? { append: selectKernel(state).append }
|
|
: undefined,
|
|
groups: undefined,
|
|
timezone: undefined,
|
|
locale: undefined,
|
|
firewall: undefined,
|
|
installation_device: undefined,
|
|
fdo: undefined,
|
|
ignition: undefined,
|
|
partitioning_mode: undefined,
|
|
fips: undefined,
|
|
};
|
|
};
|
|
|
|
const getServices = (state: RootState): Services | undefined => {
|
|
const services = selectServices(state);
|
|
if (
|
|
services.enabled.length === 0 &&
|
|
services.masked.length === 0 &&
|
|
services.disabled.length === 0
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
const enabledSvcs = services.enabled || [];
|
|
const includeFBSvc: boolean =
|
|
!!selectFirstBootScript(state) &&
|
|
!services.enabled?.includes(FIRST_BOOT_SERVICE);
|
|
if (includeFBSvc) {
|
|
enabledSvcs.push(FIRST_BOOT_SERVICE);
|
|
}
|
|
|
|
return {
|
|
enabled: enabledSvcs.length ? enabledSvcs : undefined,
|
|
masked: services.masked.length ? services.masked : undefined,
|
|
disabled: services.disabled.length ? services.disabled : undefined,
|
|
};
|
|
};
|
|
|
|
const getOpenscapProfile = (state: RootState): OpenScap | undefined => {
|
|
const profile = selectProfile(state);
|
|
if (profile) {
|
|
return { profile_id: profile };
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const getFileSystem = (state: RootState): Filesystem[] | undefined => {
|
|
const mode = selectFileSystemConfigurationType(state);
|
|
|
|
const convertToBytes = (minSize: string, conversionFactor: number) => {
|
|
return minSize.length > 0 ? parseInt(minSize) * conversionFactor : 0;
|
|
};
|
|
|
|
if (mode === 'manual') {
|
|
const partitions = selectPartitions(state);
|
|
const fileSystem = partitions.map((partition) => {
|
|
return {
|
|
min_size: convertToBytes(
|
|
partition.min_size,
|
|
getConversionFactor(partition.unit)
|
|
),
|
|
mountpoint: partition.mountpoint,
|
|
};
|
|
});
|
|
return fileSystem;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const getPackages = (state: RootState) => {
|
|
const packages = selectPackages(state);
|
|
const groups = selectGroups(state);
|
|
|
|
if (packages.length > 0 || groups.length > 0) {
|
|
return packages
|
|
.map((pkg) => pkg.name)
|
|
.concat(groups.map((grp) => '@' + grp.name));
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const getSubscription = (
|
|
state: RootState,
|
|
orgID: string
|
|
): Subscription | undefined => {
|
|
const registrationType = selectRegistrationType(state);
|
|
const activationKey = selectActivationKey(state);
|
|
|
|
if (registrationType === 'register-later') {
|
|
return undefined;
|
|
}
|
|
|
|
if (activationKey === undefined) {
|
|
throw new Error(
|
|
'Activation key unexpectedly undefined while generating subscription customization'
|
|
);
|
|
}
|
|
|
|
const initialSubscription = {
|
|
'activation-key': activationKey,
|
|
organization: Number(orgID),
|
|
'server-url': selectServerUrl(state),
|
|
'base-url': selectBaseUrl(state),
|
|
};
|
|
|
|
switch (registrationType) {
|
|
case 'register-now-rhc':
|
|
return { ...initialSubscription, insights: true, rhc: true };
|
|
case 'register-now-insights':
|
|
return { ...initialSubscription, insights: true, rhc: false };
|
|
case 'register-now':
|
|
return { ...initialSubscription, insights: false, rhc: false };
|
|
}
|
|
};
|
|
|
|
const getCustomRepositories = (state: RootState) => {
|
|
const customRepositories = selectCustomRepositories(state);
|
|
const recommendedRepositories = selectRecommendedRepositories(state);
|
|
|
|
const customAndRecommendedRepositories = [...customRepositories];
|
|
|
|
for (const repo in recommendedRepositories) {
|
|
customAndRecommendedRepositories.push(
|
|
convertSchemaToIBCustomRepo(recommendedRepositories[repo])
|
|
);
|
|
}
|
|
|
|
if (customAndRecommendedRepositories.length === 0) {
|
|
return undefined;
|
|
}
|
|
return customAndRecommendedRepositories;
|
|
};
|
|
|
|
const getPayloadRepositories = (state: RootState) => {
|
|
const payloadRepositories = selectPayloadRepositories(state);
|
|
const recommendedRepositories = selectRecommendedRepositories(state);
|
|
|
|
const payloadAndRecommendedRepositories = [...payloadRepositories];
|
|
|
|
for (const repo in recommendedRepositories) {
|
|
payloadAndRecommendedRepositories.push(
|
|
convertSchemaToIBPayloadRepo(recommendedRepositories[repo])
|
|
);
|
|
}
|
|
|
|
if (payloadAndRecommendedRepositories.length === 0) {
|
|
return undefined;
|
|
}
|
|
return payloadAndRecommendedRepositories;
|
|
};
|