Firstboot: validate shebang is defined

This commit is contained in:
Ondrej Ezr 2024-07-25 18:27:21 +02:00 committed by Klara Simickova
parent 6e138b5274
commit dbfa934b38
4 changed files with 95 additions and 26 deletions

View file

@ -28,6 +28,7 @@ import Azure from './steps/TargetEnvironment/Azure';
import Gcp from './steps/TargetEnvironment/Gcp';
import {
useFilesystemValidation,
useFirstBootValidation,
useDetailsValidation,
} from './utilities/useValidation';
import {
@ -179,9 +180,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const snapshotStepRequiresChoice = !useLatest && !snapshotDate;
const detailsValidation = useDetailsValidation();
const fileSystemValidation = useFilesystemValidation();
const [filesystemPristine, setFilesystemPristine] = useState(true);
const fileSystemValidation = useFilesystemValidation();
const firstBootValidation = useFirstBootValidation();
const detailsValidation = useDetailsValidation();
let startIndex = 1; // default index
if (isEdit) {
@ -192,7 +194,9 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
}
}
const detailsNavItem = (
// Duplicating some of the logic from the Wizard component to allow for custom nav items status
// for original code see https://github.com/patternfly/patternfly-react/blob/184c55f8d10e1d94ffd72e09212db56c15387c5e/packages/react-core/src/components/Wizard/WizardNavInternal.tsx#L128
const customStatusNavItem = (
step: WizardStepType,
activeStep: WizardStepType,
steps: WizardStepType[],
@ -203,6 +207,11 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
(s) => s.index > step.index && s.isVisited
);
// Only this code is different from the original
const status =
(step.isVisited && step.id !== activeStep?.id && step.status) ||
'default';
return (
<WizardNavItem
key={step.id}
@ -216,10 +225,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
isVisited={step.isVisited}
stepIndex={step.index}
onClick={() => goToStepByIndex(step.index)}
status={
(step.isVisited && step.id !== activeStep?.id && step.status) ||
'default'
}
status={status}
/>
);
};
@ -400,7 +406,13 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
name="First boot script configuration"
id="wizard-first-boot"
key="wizard-first-boot"
footer={<CustomWizardFooter disableNext={false} />}
navItem={customStatusNavItem}
status={firstBootValidation.disabledNext ? 'error' : 'default'}
footer={
<CustomWizardFooter
disableNext={firstBootValidation.disabledNext}
/>
}
>
<FirstBootStep />
</WizardStep>
@ -409,7 +421,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
name="Details"
id={'step-details'}
isDisabled={snapshotStepRequiresChoice}
navItem={detailsNavItem}
navItem={customStatusNavItem}
status={detailsValidation.disabledNext ? 'error' : 'default'}
footer={
<CustomWizardFooter

View file

@ -1,13 +1,23 @@
import React from 'react';
import { CodeEditor, Language } from '@patternfly/react-code-editor';
import { Text, Form, Title, Alert } from '@patternfly/react-core';
import {
Text,
Form,
FormGroup,
FormHelperText,
Title,
Alert,
HelperText,
HelperTextItem,
} from '@patternfly/react-core';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
import {
selectFirstBootScript,
setFirstBootScript,
} from '../../../../store/wizardSlice';
import { useFirstBootValidation } from '../../utilities/useValidation';
const detectScriptType = (scriptString: string): Language => {
const lines = scriptString.split('\n');
@ -32,6 +42,7 @@ const FirstBootStep = () => {
const dispatch = useAppDispatch();
const selectedScript = useAppSelector(selectFirstBootScript);
const language = detectScriptType(selectedScript);
const { errors } = useFirstBootValidation();
return (
<Form>
@ -55,20 +66,31 @@ const FirstBootStep = () => {
privacy.
</Text>
</Alert>
<CodeEditor
isUploadEnabled
isDownloadEnabled
isCopyEnabled
isLanguageLabelVisible
language={language}
onCodeChange={(code) => dispatch(setFirstBootScript(code))}
code={selectedScript}
height="35vh"
emptyStateButton={<span data-testid="firstboot_browse">Browse</span>}
emptyStateLink={
<span data-testid="firstboot_write_manual">Start from scratch</span>
}
/>
<FormGroup>
<CodeEditor
isUploadEnabled
isDownloadEnabled
isCopyEnabled
isLanguageLabelVisible
language={language}
onCodeChange={(code) => dispatch(setFirstBootScript(code))}
code={selectedScript}
height="35vh"
emptyStateButton={<span data-testid="firstboot_browse">Browse</span>}
emptyStateLink={
<span data-testid="firstboot_write_manual">Start from scratch</span>
}
/>
{errors.script && (
<FormHelperText>
<HelperText>
<HelperTextItem variant="error" hasIcon>
{errors.script}
</HelperTextItem>
</HelperText>
</FormHelperText>
)}
</FormGroup>
</Form>
);
};

View file

@ -10,6 +10,7 @@ import {
selectBlueprintName,
selectBlueprintDescription,
selectFileSystemPartitionMode,
selectFirstBootScript,
selectPartitions,
} from '../../../store/wizardSlice';
import {
@ -28,8 +29,11 @@ export type StepValidation = {
export function useIsBlueprintValid(): boolean {
const filesystem = useFilesystemValidation();
const firstBoot = useFirstBootValidation();
const details = useDetailsValidation();
return !filesystem.disabledNext && !details.disabledNext;
return (
!filesystem.disabledNext && !firstBoot.disabledNext && !details.disabledNext
);
}
export function useFilesystemValidation(): StepValidation {
@ -56,6 +60,21 @@ export function useFilesystemValidation(): StepValidation {
return { errors, disabledNext };
}
export function useFirstBootValidation(): StepValidation {
const script = useAppSelector(selectFirstBootScript);
let hasShebang = false;
if (script) {
hasShebang = script.split('\n')[0].startsWith('#!');
}
const valid = !script || hasShebang;
return {
errors: {
script: valid ? '' : 'Missing shebang at first line, e.g. #!/bin/bash',
},
disabledNext: !valid,
};
}
export function useDetailsValidation(): StepValidation {
const name = useAppSelector(selectBlueprintName);
const description = useAppSelector(selectBlueprintDescription);

View file

@ -13,7 +13,7 @@ import {
firstBootCreateBlueprintRequest,
firstBootData,
} from '../../../../fixtures/editMode';
import { clickNext } from '../../../../testUtils';
import { clickNext, getNextButton } from '../../../../testUtils';
import {
blueprintRequest,
clickRegisterLater,
@ -104,6 +104,22 @@ describe('First Boot step', () => {
await goToFirstBootStep();
await screen.findByText('First boot configuration');
});
// test('should validate shebang', async () => {
// await renderCreateMode();
// await goToFirstBootStep();
// await openCodeEditor();
// const editor = await screen.findByRole('textbox');
// await userEvent.type(editor, 'echo "Hello, world!"');
// expect(await screen.findByText('Missing shebang')).toBeInTheDocument();
// expect(await getNextButton()).toBeDisabled();
// await userEvent.clear(editor);
// await userEvent.type(editor, '#!/bin/bash');
// expect(screen.queryByText('Missing shebang')).not.toBeInTheDocument();
// expect(await getNextButton()).toBeEnabled();
// });
// describe('validate first boot request ', () => {
// test('should validate first boot request', async () => {
// await renderCreateMode();