feat(HMS-3582): add first boot step to wizard
This commit is contained in:
parent
c88a0323f2
commit
6f9c4f3864
23 changed files with 401 additions and 12 deletions
62
package-lock.json
generated
62
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"@data-driven-forms/pf4-component-mapper": "3.22.4",
|
||||
"@data-driven-forms/react-form-renderer": "3.22.4",
|
||||
"@patternfly/patternfly": "5.3.0",
|
||||
"@patternfly/react-code-editor": "5.3.0",
|
||||
"@patternfly/react-core": "5.3.0",
|
||||
"@patternfly/react-table": "5.2.4",
|
||||
"@redhat-cloud-services/frontend-components": "4.2.7",
|
||||
|
|
@ -3208,6 +3209,30 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
|
||||
"integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.21.0 < 1"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/react": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz",
|
||||
"integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.25.0 < 1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mswjs/cookies": {
|
||||
"version": "0.2.2",
|
||||
"dev": true,
|
||||
|
|
@ -3363,6 +3388,32 @@
|
|||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-5.3.0.tgz",
|
||||
"integrity": "sha512-93uWA15bOJDgu8NF2iReWbbNtWdtM+v7iaDpK33mJChgej+whiFpGLtQPI2jFk1aVW3rDpbt4qm4OaNinpzSsg=="
|
||||
},
|
||||
"node_modules/@patternfly/react-code-editor": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-5.3.0.tgz",
|
||||
"integrity": "sha512-myPZh+POJpnXOrDtAYq83zMa26YCuM6KsAgLeaW6riajLyZ1VE3lHztX300xaJR1sDbIvxrdZCNDqmyoJI2z1g==",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@patternfly/react-core": "^5.3.0",
|
||||
"@patternfly/react-icons": "^5.3.0",
|
||||
"@patternfly/react-styles": "^5.3.0",
|
||||
"react-dropzone": "14.2.3",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18",
|
||||
"react-dom": "^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/react-code-editor/node_modules/@patternfly/react-icons": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.3.1.tgz",
|
||||
"integrity": "sha512-puiMzX39asr+j5adA3J1xuK5NjwKH4UAp57GoLTga9DcsPu0g8u0H3WHtunYCJzUQ8n7FvaMYFH1H0WcWlDIQQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18",
|
||||
"react-dom": "^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/react-component-groups": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.0.0.tgz",
|
||||
|
|
@ -14320,6 +14371,12 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.47.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.47.0.tgz",
|
||||
"integrity": "sha512-VabVvHvQ9QmMwXu4du008ZDuyLnHs9j7ThVFsiJoXSOQk18+LF89N4ADzPbFenm0W4V2bGHnFBztIRQTgBfxzw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/moo-color": {
|
||||
"version": "1.0.3",
|
||||
"dev": true,
|
||||
|
|
@ -17395,6 +17452,11 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"@data-driven-forms/react-form-renderer": "3.22.4",
|
||||
"@patternfly/patternfly": "5.3.0",
|
||||
"@patternfly/react-core": "5.3.0",
|
||||
"@patternfly/react-code-editor": "5.3.0",
|
||||
"@patternfly/react-table": "5.2.4",
|
||||
"@redhat-cloud-services/frontend-components": "4.2.7",
|
||||
"@redhat-cloud-services/frontend-components-notifications": "4.1.0",
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import {
|
|||
WizardStepType,
|
||||
useWizardContext,
|
||||
} from '@patternfly/react-core';
|
||||
import { useFlag } from '@unleash/proxy-client-react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import DetailsStep from './steps/Details';
|
||||
import FileSystemStep from './steps/FileSystem';
|
||||
import { FileSystemStepFooter } from './steps/FileSystem/FileSystemConfiguration';
|
||||
import FirstBootStep from './steps/FirstBoot';
|
||||
import ImageOutputStep from './steps/ImageOutput';
|
||||
import OscapStep from './steps/Oscap';
|
||||
import PackagesStep from './steps/Packages';
|
||||
|
|
@ -126,6 +128,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
|
||||
// =========================TO REMOVE=======================
|
||||
|
||||
const firstbootFlag = useFlag('image-builder.firstboot.enabled');
|
||||
const isFirstBootEnabled = isBeta() && firstbootFlag;
|
||||
// IMPORTANT: Ensure the wizard starts with a fresh initial state
|
||||
useEffect(() => {
|
||||
dispatch(initializeWizard());
|
||||
|
|
@ -176,13 +180,22 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
) => setCurrentStep(currentStep);
|
||||
|
||||
const detailsValidation = useAppSelector(selectStepValidation('details'));
|
||||
let startIndex = 1; // default index
|
||||
|
||||
if (isEdit) {
|
||||
if (snapshottingEnabled) {
|
||||
startIndex = isFirstBootEnabled ? 15 : 14;
|
||||
} else {
|
||||
startIndex = isFirstBootEnabled ? 14 : 13;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImageBuilderHeader />
|
||||
<section className="pf-l-page__main-section pf-c-page__main-section">
|
||||
<Wizard
|
||||
startIndex={isEdit ? (snapshottingEnabled ? 14 : 13) : 1}
|
||||
startIndex={startIndex}
|
||||
onClose={() => navigate(resolveRelPath(''))}
|
||||
onStepChange={onStepChange}
|
||||
isVisitRequired
|
||||
|
|
@ -334,6 +347,16 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
</WizardStep>,
|
||||
]}
|
||||
/>
|
||||
{isFirstBootEnabled && (
|
||||
<WizardStep
|
||||
name="First boot script configuration"
|
||||
id="wizard-first-boot"
|
||||
key="wizard-first-boot"
|
||||
footer={<CustomWizardFooter disableNext={false} />}
|
||||
>
|
||||
<FirstBootStep />
|
||||
</WizardStep>
|
||||
)}
|
||||
<WizardStep
|
||||
name="Details"
|
||||
id="step-details"
|
||||
|
|
|
|||
52
src/Components/CreateImageWizardV2/steps/FirstBoot/index.tsx
Normal file
52
src/Components/CreateImageWizardV2/steps/FirstBoot/index.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
|
||||
import { CodeEditor, Language } from '@patternfly/react-code-editor';
|
||||
import { Text, Form, Title, Alert } from '@patternfly/react-core';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
selectFirstBootScript,
|
||||
setFirstBootScript,
|
||||
} from '../../../../store/wizardSlice';
|
||||
|
||||
const FirstBootStep = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedScript = useAppSelector(selectFirstBootScript);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel="h1" size="xl">
|
||||
First boot configuration
|
||||
</Title>
|
||||
<Text>
|
||||
Configure the image with a custom script that will execute on its first
|
||||
boot.
|
||||
</Text>
|
||||
<Alert
|
||||
variant="warning"
|
||||
isExpandable
|
||||
isInline
|
||||
title="Important: please do not include sensitive information"
|
||||
>
|
||||
<Text>
|
||||
Please ensure that your script does not contain any secrets,
|
||||
passwords, or other sensitive data. All scripts should be crafted
|
||||
without including confidential information to maintain security and
|
||||
privacy.
|
||||
</Text>
|
||||
</Alert>
|
||||
<CodeEditor
|
||||
isUploadEnabled
|
||||
isDownloadEnabled
|
||||
isCopyEnabled
|
||||
isLanguageLabelVisible
|
||||
language={Language.shell}
|
||||
onCodeChange={(code) => dispatch(setFirstBootScript(code))}
|
||||
code={selectedScript}
|
||||
height="35vh"
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default FirstBootStep;
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import {
|
||||
ContentList,
|
||||
FSCList,
|
||||
FirstBootList,
|
||||
ImageDetailsList,
|
||||
ImageOutputList,
|
||||
OscapList,
|
||||
|
|
@ -48,6 +49,7 @@ const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => {
|
|||
const [isExpandedRegistration, setIsExpandedRegistration] = useState(false);
|
||||
const [isExpandedImageDetail, setIsExpandedImageDetail] = useState(false);
|
||||
const [isExpandedOscapDetail, setIsExpandedOscapDetail] = useState(false);
|
||||
const [isExpandableFirstBoot, setIsExpandedFirstBoot] = useState(false);
|
||||
|
||||
const onToggleImageOutput = (isExpandedImageOutput: boolean) =>
|
||||
setIsExpandedImageOutput(isExpandedImageOutput);
|
||||
|
|
@ -63,6 +65,8 @@ const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => {
|
|||
setIsExpandedImageDetail(isExpandedImageDetail);
|
||||
const onToggleOscapDetails = (isExpandedOscapDetail: boolean) =>
|
||||
setIsExpandedOscapDetail(isExpandedOscapDetail);
|
||||
const onToggleFirstBoot = (isExpandableFirstBoot: boolean) =>
|
||||
setIsExpandedFirstBoot(isExpandableFirstBoot);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -175,6 +179,17 @@ const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => {
|
|||
{/* Intentional prop drilling for simplicity - To be removed */}
|
||||
<ContentList snapshottingEnabled={snapshottingEnabled} />
|
||||
</ExpandableSection>
|
||||
<ExpandableSection
|
||||
toggleContent={'First boot'}
|
||||
onToggle={(_event, isExpandableFirstBoot) =>
|
||||
onToggleFirstBoot(isExpandableFirstBoot)
|
||||
}
|
||||
isExpanded={isExpandableFirstBoot}
|
||||
isIndented
|
||||
data-testid="firstboot-expandable"
|
||||
>
|
||||
<FirstBootList />
|
||||
</ExpandableSection>
|
||||
{(blueprintName || blueprintDescription) && (
|
||||
<ExpandableSection
|
||||
toggleContent={'Image details'}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import {
|
|||
selectSnapshotDate,
|
||||
selectUseLatest,
|
||||
selectPartitions,
|
||||
selectFirstBootScript,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import {
|
||||
convertMMDDYYYYToYYYYMMDD,
|
||||
|
|
@ -754,3 +755,23 @@ export const ImageDetailsList = () => {
|
|||
export const OscapList = () => {
|
||||
return <OscapProfileInformation />;
|
||||
};
|
||||
|
||||
export const FirstBootList = () => {
|
||||
const isFirstbootEnabled = !!useAppSelector(selectFirstBootScript);
|
||||
|
||||
return (
|
||||
<TextContent>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
First boot script
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{isFirstbootEnabled ? 'Enabled' : 'Disabled'}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
|
||||
import { parseSizeUnit } from './parseSizeUnit';
|
||||
|
||||
import {
|
||||
FIRST_BOOT_SERVICE,
|
||||
FIRST_BOOT_SERVICE_DATA,
|
||||
} from '../../../constants';
|
||||
import { RootState } from '../../../store';
|
||||
import {
|
||||
AwsUploadRequestOptions,
|
||||
|
|
@ -11,6 +15,7 @@ import {
|
|||
CreateBlueprintRequest,
|
||||
Customizations,
|
||||
DistributionProfileItem,
|
||||
File,
|
||||
Filesystem,
|
||||
GcpUploadRequestOptions,
|
||||
ImageRequest,
|
||||
|
|
@ -51,6 +56,7 @@ import {
|
|||
selectPartitions,
|
||||
selectSnapshotDate,
|
||||
selectUseLatest,
|
||||
selectFirstBootScript,
|
||||
} from '../../../store/wizardSlice';
|
||||
import {
|
||||
convertMMDDYYYYToYYYYMMDD,
|
||||
|
|
@ -170,6 +176,9 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
|||
?.profile_id as DistributionProfileItem,
|
||||
},
|
||||
fileSystem: fileSystem,
|
||||
firstBoot: {
|
||||
script: getFirstBootScript(request.customizations.files),
|
||||
},
|
||||
architecture: request.image_requests[0].architecture,
|
||||
distribution: request.distribution,
|
||||
imageTypes: request.image_requests.map((image) => image.image_type),
|
||||
|
|
@ -222,6 +231,13 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
|||
};
|
||||
};
|
||||
|
||||
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));
|
||||
|
|
@ -324,7 +340,23 @@ const getCustomizations = (
|
|||
return {
|
||||
containers: undefined,
|
||||
directories: undefined,
|
||||
files: 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),
|
||||
|
|
@ -332,7 +364,7 @@ const getCustomizations = (
|
|||
openscap: getOpenscapProfile(state),
|
||||
filesystem: getFileSystem(state),
|
||||
users: undefined,
|
||||
services: getServices(serverStore),
|
||||
services: getServices(serverStore, state),
|
||||
hostname: undefined,
|
||||
kernel: serverStore.kernel?.append
|
||||
? { append: serverStore.kernel?.append }
|
||||
|
|
@ -349,16 +381,33 @@ const getCustomizations = (
|
|||
};
|
||||
};
|
||||
|
||||
const getServices = (serverStore: ServerStore): Services | undefined => {
|
||||
const enabledServices = serverStore.services?.enabled;
|
||||
const disabledServices = serverStore.services?.disabled;
|
||||
const maskedServices = serverStore.services?.masked;
|
||||
const getServices = (
|
||||
serverStore: ServerStore,
|
||||
state: RootState
|
||||
): Services | undefined => {
|
||||
const serverEnabledServices: string[] | undefined =
|
||||
serverStore.services?.enabled;
|
||||
const serverDisabledServicesFromServer: string[] | undefined =
|
||||
serverStore.services?.disabled;
|
||||
const serverMaskedServices = serverStore.services?.masked;
|
||||
const firstbootFlag: boolean =
|
||||
!!selectFirstBootScript(state) &&
|
||||
!serverEnabledServices?.includes(FIRST_BOOT_SERVICE);
|
||||
|
||||
if (enabledServices || disabledServices || maskedServices) {
|
||||
const enabledServices = [
|
||||
...(serverEnabledServices ? serverEnabledServices : []),
|
||||
...(firstbootFlag ? [FIRST_BOOT_SERVICE] : []),
|
||||
];
|
||||
|
||||
if (
|
||||
enabledServices.length ||
|
||||
serverDisabledServicesFromServer ||
|
||||
serverMaskedServices
|
||||
) {
|
||||
return {
|
||||
enabled: enabledServices,
|
||||
disabled: disabledServices,
|
||||
masked: maskedServices,
|
||||
disabled: serverDisabledServicesFromServer,
|
||||
masked: serverMaskedServices,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -195,3 +195,7 @@ export const EPEL_9_REPO_DEFINITION = {
|
|||
};
|
||||
|
||||
export const DEBOUNCED_SEARCH_WAIT_TIME = 500;
|
||||
export const FIRST_BOOT_SERVICE_DATA =
|
||||
'W1VuaXRdCkRlc2NyaXB0aW9uPVJ1biBmaXJzdCBib290IHNjcmlwdApDb25kaXRpb25QYXRoRXhpc3RzPS91c3IvbG9jYWwvc2Jpbi9jdXN0b20tZmlyc3QtYm9vdApXYW50cz1uZXR3b3JrLW9ubGluZS50YXJnZXQKQWZ0ZXI9bmV0d29yay1vbmxpbmUudGFyZ2V0CkFmdGVyPW9zYnVpbGQtZmlyc3QtYm9vdC5zZXJ2aWNlCgpbU2VydmljZV0KVHlwZT1vbmVzaG90CkV4ZWNTdGFydD0vdXNyL2xvY2FsL3NiaW4vY3VzdG9tLWZpcnN0LWJvb3QKRXhlY1N0YXJ0UG9zdD1tdiAvdXNyL2xvY2FsL3NiaW4vY3VzdG9tLWZpcnN0LWJvb3QgL3Vzci9sb2NhbC9zYmluL2N1c3RvbS1maXJzdC1ib290LmRvbmUKCltJbnN0YWxsXQpXYW50ZWRCeT1tdWx0aS11c2VyLnRhcmdldAo=';
|
||||
|
||||
export const FIRST_BOOT_SERVICE = 'custom-first-boot';
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ export type wizardState = {
|
|||
useLatest: boolean;
|
||||
snapshotDate: string;
|
||||
};
|
||||
firstBoot: {
|
||||
script: string;
|
||||
};
|
||||
repositories: {
|
||||
customRepositories: CustomRepository[];
|
||||
payloadRepositories: Repository[];
|
||||
|
|
@ -155,6 +158,7 @@ const initialState: wizardState = {
|
|||
blueprintDescription: '',
|
||||
},
|
||||
stepValidations: {},
|
||||
firstBoot: { script: '' },
|
||||
};
|
||||
|
||||
export const selectServerUrl = (state: RootState) => {
|
||||
|
|
@ -297,6 +301,10 @@ export const selectInputValidation =
|
|||
return isValid ? 'success' : 'error';
|
||||
};
|
||||
|
||||
export const selectFirstBootScript = (state: RootState) => {
|
||||
return state.wizard.firstBoot?.script;
|
||||
};
|
||||
|
||||
export const wizardSlice = createSlice({
|
||||
name: 'wizard',
|
||||
initialState,
|
||||
|
|
@ -599,6 +607,9 @@ export const wizardSlice = createSlice({
|
|||
changeBlueprintDescription: (state, action: PayloadAction<string>) => {
|
||||
state.details.blueprintDescription = action.payload;
|
||||
},
|
||||
setFirstBootScript: (state, action: PayloadAction<string>) => {
|
||||
state.firstBoot.script = action.payload;
|
||||
},
|
||||
setStepInputValidation: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
|
|
@ -677,5 +688,6 @@ export const {
|
|||
changeBlueprintDescription,
|
||||
loadWizardState,
|
||||
setStepInputValidation,
|
||||
setFirstBootScript,
|
||||
} = wizardSlice.actions;
|
||||
export default wizardSlice.reducer;
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ describe('Step Packages', () => {
|
|||
test('clicking Next loads Image name', async () => {
|
||||
await setUp();
|
||||
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
|
||||
await screen.findByRole('heading', {
|
||||
|
|
|
|||
|
|
@ -418,6 +418,7 @@ describe('Step Upload to AWS', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
|
||||
|
|
@ -611,6 +612,7 @@ describe('Step Registration', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
const review = await screen.findByTestId('review-registration');
|
||||
|
|
@ -658,6 +660,7 @@ describe('Step Registration', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
const review = await screen.findByTestId('review-registration');
|
||||
|
|
@ -706,6 +709,7 @@ describe('Step Registration', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
const review = await screen.findByTestId('review-registration');
|
||||
|
|
@ -737,6 +741,7 @@ describe('Step Registration', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
await screen.findByText('Register the system later');
|
||||
|
|
@ -896,7 +901,9 @@ describe('Step Details', () => {
|
|||
await clickNext();
|
||||
// skip fsc
|
||||
await clickNext();
|
||||
// skip snapshots
|
||||
// skip snapshot
|
||||
await clickNext();
|
||||
//skip firstBoot
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
|
|
@ -979,6 +986,8 @@ describe('Step Review', () => {
|
|||
// skip packages
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
// skip firstboot
|
||||
await clickNext();
|
||||
// skip Details
|
||||
const blueprintName = await screen.findByRole('textbox', {
|
||||
name: /blueprint name/i,
|
||||
|
|
@ -1042,6 +1051,8 @@ describe('Step Review', () => {
|
|||
// skip repositories
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
// skip First boot
|
||||
await clickNext();
|
||||
const blueprintName = await screen.findByRole('textbox', {
|
||||
name: /blueprint name/i,
|
||||
});
|
||||
|
|
@ -1223,7 +1234,7 @@ describe('Keyboard accessibility', () => {
|
|||
|
||||
// TODO: Focus on textbox on Packages step
|
||||
await clickNext();
|
||||
|
||||
await clickNext();
|
||||
// TODO: Focus on textbox on Details step
|
||||
await clickNext();
|
||||
}, 20000);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ const goToDetailsStep = async () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
const enterBlueprintDescription = async () => {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ const goToReviewStep = async () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
import { screen } from '@testing-library/react';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
import {
|
||||
CREATE_BLUEPRINT,
|
||||
FIRST_BOOT_SERVICE,
|
||||
FIRST_BOOT_SERVICE_DATA,
|
||||
} from '../../../../../constants';
|
||||
import { File as ImageBuilderFile } from '../../../../../store/imageBuilderApi';
|
||||
import { clickNext } from '../../../../testUtils';
|
||||
import {
|
||||
blueprintRequest,
|
||||
clickRegisterLater,
|
||||
enterBlueprintName,
|
||||
interceptBlueprintRequest,
|
||||
renderCreateMode,
|
||||
} from '../../wizardTestUtils';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
auth: {
|
||||
getUser: () => {
|
||||
return {
|
||||
identity: {
|
||||
internal: {
|
||||
org_id: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
isBeta: () => true,
|
||||
isProd: () => true,
|
||||
getEnvironment: () => 'prod',
|
||||
}),
|
||||
}));
|
||||
|
||||
const goToFirstBootStep = async (): Promise<void> => {
|
||||
const guestImageCheckBox = await screen.findByRole('checkbox', {
|
||||
name: /virtualization guest image checkbox/i,
|
||||
});
|
||||
await userEvent.click(guestImageCheckBox);
|
||||
await clickNext();
|
||||
await clickNext(); // Registration
|
||||
await clickRegisterLater();
|
||||
await clickNext(); // OpenSCAP
|
||||
await clickNext(); // File System
|
||||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // Snapshot
|
||||
await clickNext(); // First Boot
|
||||
};
|
||||
|
||||
const openCodeEditor = async (): Promise<void> => {
|
||||
const startBtn = await screen.findByRole('button', {
|
||||
name: /Start from scratch/i,
|
||||
});
|
||||
await userEvent.click(startBtn);
|
||||
};
|
||||
|
||||
const uploadFile = async (): Promise<void> => {
|
||||
const fileInput: HTMLElement | null =
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
document.querySelector('input[type="file"]');
|
||||
|
||||
if (fileInput) {
|
||||
const file = new File([SCRIPT], 'script.sh', { type: 'text/x-sh' });
|
||||
await userEvent.upload(fileInput, file);
|
||||
}
|
||||
};
|
||||
|
||||
const goToReviewStep = async (): Promise<void> => {
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
};
|
||||
|
||||
const SCRIPT = `#!/bin/bash
|
||||
systemctl enable cockpit.socket`;
|
||||
|
||||
const BASE64_SCRIPT = btoa(SCRIPT);
|
||||
const firstBootData: ImageBuilderFile[] = [
|
||||
{
|
||||
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: BASE64_SCRIPT,
|
||||
data_encoding: 'base64',
|
||||
mode: '0774',
|
||||
ensure_parents: true,
|
||||
},
|
||||
];
|
||||
|
||||
describe('First Boot step', () => {
|
||||
test('should render First Boot step', async () => {
|
||||
await renderCreateMode();
|
||||
await goToFirstBootStep();
|
||||
expect(screen.getByText('First boot configuration')).toBeInTheDocument();
|
||||
});
|
||||
describe('validate first boot request ', () => {
|
||||
test('should validate first boot request', async () => {
|
||||
await renderCreateMode();
|
||||
await goToFirstBootStep();
|
||||
await openCodeEditor();
|
||||
await uploadFile();
|
||||
await goToReviewStep();
|
||||
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
|
||||
|
||||
const expectedRequest = {
|
||||
...blueprintRequest,
|
||||
customizations: {
|
||||
files: firstBootData,
|
||||
services: { enabled: [FIRST_BOOT_SERVICE] },
|
||||
},
|
||||
};
|
||||
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -60,6 +60,7 @@ const clickToReview = async () => {
|
|||
await clickNext(); // skip SnapshotRepositories
|
||||
await clickNext(); // skip Repositories
|
||||
await clickNext(); // skip Packages
|
||||
await clickNext(); // skip First Boot
|
||||
const nameInput = await screen.findByRole('textbox', {
|
||||
name: /blueprint name/i,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ const goToReviewStep = async () => {
|
|||
await clickNext(); // Snapshot repositories
|
||||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // FirstBoot
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName('oscap');
|
||||
await clickNext(); // Review
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ const goToPackagesStep = async () => {
|
|||
};
|
||||
|
||||
const goToReviewStep = async () => {
|
||||
await clickNext(); // First Boot
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ const goToReviewStep = async () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await clickNext();
|
||||
await enterBlueprintName();
|
||||
await clickNext();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ const goToRepositoriesStep = async () => {
|
|||
const goToReviewStep = async () => {
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext();
|
||||
await clickNext(); // First Boot
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const goToReview = async () => {
|
|||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // Details
|
||||
await clickNext(); // FirstBoot
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ const goToReview = async () => {
|
|||
await clickNext(); // Snapshot repositories
|
||||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // FirstBoot
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const goToReview = async () => {
|
|||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // Details
|
||||
await clickNext(); // FirstBoot
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
};
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ const goToReviewStep = async () => {
|
|||
await clickNext(); // Snapshots
|
||||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // First boot
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue