V2Wizard: add oscap to wizard2
This adds the OpenSCAP step to the V2Wizard, adds new values to the wizardSlice and enables relevant tests in the `CreateImageWizard.test.tsx` test suite
This commit is contained in:
parent
c7a80d0e85
commit
c4e1709de8
6 changed files with 610 additions and 1 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import ImageOutputStep from './steps/ImageOutput';
|
||||
import OscapStep from './steps/Oscap';
|
||||
import RegistrationStep from './steps/Registration';
|
||||
import Aws from './steps/TargetEnvironment/Aws';
|
||||
import Gcp from './steps/TargetEnvironment/Gcp';
|
||||
|
|
@ -159,6 +160,13 @@ const CreateImageWizard = () => {
|
|||
>
|
||||
<RegistrationStep />
|
||||
</WizardStep>
|
||||
<WizardStep
|
||||
name="OpenSCAP"
|
||||
id="step-oscap"
|
||||
footer={<CustomWizardFooter disableNext={false} />}
|
||||
>
|
||||
<OscapStep />
|
||||
</WizardStep>
|
||||
<WizardStep
|
||||
name="Review"
|
||||
id="step-review"
|
||||
|
|
|
|||
257
src/Components/CreateImageWizardV2/steps/Oscap/Oscap.tsx
Normal file
257
src/Components/CreateImageWizardV2/steps/Oscap/Oscap.tsx
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
FormGroup,
|
||||
Spinner,
|
||||
Popover,
|
||||
TextContent,
|
||||
Text,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
|
||||
import OscapProfileInformation from './OscapProfileInformation';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
DistributionProfileItem,
|
||||
useGetOscapCustomizationsQuery,
|
||||
useGetOscapProfilesQuery,
|
||||
} from '../../../../store/imageBuilderApi';
|
||||
import {
|
||||
changeOscapProfile,
|
||||
selectDistribution,
|
||||
selectProfile,
|
||||
} from '../../../../store/wizardSlice';
|
||||
|
||||
const ProfileSelector = () => {
|
||||
const oscapProfile = useAppSelector((state) => selectProfile(state));
|
||||
const release = useAppSelector((state) => selectDistribution(state));
|
||||
const dispatch = useAppDispatch();
|
||||
const [profileName, setProfileName] = useState<string | undefined>('None');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const {
|
||||
data: profiles,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
refetch,
|
||||
} = useGetOscapProfilesQuery({
|
||||
distribution: release,
|
||||
});
|
||||
|
||||
const { data } = useGetOscapCustomizationsQuery(
|
||||
{
|
||||
distribution: release,
|
||||
// @ts-ignore if oscapProfile is undefined the query is going to get skipped, so it's safe here to ignore the linter here
|
||||
profile: oscapProfile,
|
||||
},
|
||||
{
|
||||
skip: !oscapProfile,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
data &&
|
||||
data.openscap &&
|
||||
typeof data.openscap.profile_name === 'string'
|
||||
) {
|
||||
setProfileName(data.openscap.profile_name);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!isOpen) {
|
||||
refetch();
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
dispatch(changeOscapProfile(undefined));
|
||||
setProfileName(undefined);
|
||||
};
|
||||
|
||||
const handleSelect = (
|
||||
_event: React.MouseEvent<Element, MouseEvent>,
|
||||
selection: DistributionProfileItem
|
||||
) => {
|
||||
dispatch(changeOscapProfile(selection));
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const options = [
|
||||
<OScapNoneOption setProfileName={setProfileName} key="oscap-none-option" />,
|
||||
];
|
||||
if (isSuccess) {
|
||||
options.concat(
|
||||
profiles.map((profile_id) => {
|
||||
return (
|
||||
<OScapSelectOption
|
||||
key={profile_id}
|
||||
profile_id={profile_id}
|
||||
setProfileName={setProfileName}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isFetching) {
|
||||
options.push(
|
||||
<SelectOption
|
||||
isNoResultsOption={true}
|
||||
data-testid="policies-loading"
|
||||
key={'None'}
|
||||
>
|
||||
<Spinner size="md" />
|
||||
</SelectOption>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={true}
|
||||
data-testid="profiles-form-group"
|
||||
label={
|
||||
<>
|
||||
OpenSCAP profile
|
||||
<Popover
|
||||
maxWidth="30rem"
|
||||
position="left"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
To run a manual compliance scan in OpenSCAP, download this
|
||||
image.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button variant="plain" aria-label="About OpenSCAP" isInline>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
ouiaId="profileSelect"
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={handleToggle}
|
||||
onSelect={handleSelect}
|
||||
onClear={handleClear}
|
||||
selections={profileName}
|
||||
isOpen={isOpen}
|
||||
placeholderText="Select a profile"
|
||||
typeAheadAriaLabel="Select a profile"
|
||||
isDisabled={!isSuccess}
|
||||
onFilter={(_event, value) => {
|
||||
if (profiles) {
|
||||
return [
|
||||
<OScapNoneOption
|
||||
setProfileName={setProfileName}
|
||||
key="oscap-none-option"
|
||||
/>,
|
||||
].concat(
|
||||
profiles.map((profile_id, index) => {
|
||||
return (
|
||||
<OScapSelectOption
|
||||
key={index}
|
||||
profile_id={profile_id}
|
||||
setProfileName={setProfileName}
|
||||
input={value}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{options}
|
||||
</Select>
|
||||
{isError && (
|
||||
<Alert
|
||||
title="Error fetching the profiles"
|
||||
variant="danger"
|
||||
isPlain
|
||||
isInline
|
||||
>
|
||||
Cannot get the list of profiles
|
||||
</Alert>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
type OScapNoneOptionPropType = {
|
||||
setProfileName: (name: string) => void;
|
||||
};
|
||||
|
||||
const OScapNoneOption = ({ setProfileName }: OScapNoneOptionPropType) => {
|
||||
return (
|
||||
<SelectOption
|
||||
value={undefined}
|
||||
onClick={() => {
|
||||
setProfileName('None');
|
||||
}}
|
||||
>
|
||||
<p>{'None'}</p>
|
||||
</SelectOption>
|
||||
);
|
||||
};
|
||||
|
||||
type OScapSelectOptionPropType = {
|
||||
profile_id: DistributionProfileItem;
|
||||
setProfileName: (name: string) => void;
|
||||
input?: string;
|
||||
};
|
||||
|
||||
const OScapSelectOption = ({
|
||||
profile_id,
|
||||
setProfileName,
|
||||
input,
|
||||
}: OScapSelectOptionPropType) => {
|
||||
const release = useAppSelector((state) => selectDistribution(state));
|
||||
const { data } = useGetOscapCustomizationsQuery({
|
||||
distribution: release,
|
||||
profile: profile_id,
|
||||
});
|
||||
|
||||
if (
|
||||
input &&
|
||||
!data?.openscap?.profile_name?.toLowerCase().includes(input.toLowerCase())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectOption
|
||||
key={profile_id}
|
||||
value={profile_id}
|
||||
onClick={() => {
|
||||
if (data?.openscap?.profile_name) {
|
||||
setProfileName(data?.openscap?.profile_name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p>{data?.openscap?.profile_name}</p>
|
||||
</SelectOption>
|
||||
);
|
||||
};
|
||||
|
||||
export const Oscap = () => {
|
||||
return (
|
||||
<>
|
||||
<ProfileSelector />
|
||||
<OscapProfileInformation />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Spinner,
|
||||
TextContent,
|
||||
TextList,
|
||||
TextListItem,
|
||||
TextListItemVariants,
|
||||
TextListVariants,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { RELEASES } from '../../../../constants';
|
||||
import { useAppSelector } from '../../../../store/hooks';
|
||||
import { useGetOscapCustomizationsQuery } from '../../../../store/imageBuilderApi';
|
||||
import {
|
||||
selectDistribution,
|
||||
selectProfile,
|
||||
} from '../../../../store/wizardSlice';
|
||||
|
||||
const OscapProfileInformation = (): JSX.Element => {
|
||||
const release = useAppSelector((state) => selectDistribution(state));
|
||||
const oscapProfile = useAppSelector((state) => selectProfile(state));
|
||||
|
||||
const {
|
||||
data: oscapProfileInfo,
|
||||
isFetching: isFetchingOscapProfileInfo,
|
||||
isSuccess: isSuccessOscapProfileInfo,
|
||||
} = useGetOscapCustomizationsQuery(
|
||||
{
|
||||
distribution: release,
|
||||
// @ts-ignore if oscapProfile is undefined the query is going to get skipped, so it's safe here to ignore the linter here
|
||||
profile: oscapProfile,
|
||||
},
|
||||
{
|
||||
skip: !oscapProfile,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFetchingOscapProfileInfo && <Spinner size="lg" />}
|
||||
{isSuccessOscapProfileInfo && (
|
||||
<TextContent>
|
||||
<br />
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Profile description:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{oscapProfileInfo.openscap?.profile_description}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Operating system:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{RELEASES.get(release)}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
<TextList component={TextListVariants.dl}>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-u-min-width"
|
||||
>
|
||||
Reference ID:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
{oscapProfileInfo.openscap?.profile_id}
|
||||
</TextListItem>
|
||||
</TextList>
|
||||
</TextContent>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OscapProfileInformation;
|
||||
31
src/Components/CreateImageWizardV2/steps/Oscap/index.tsx
Normal file
31
src/Components/CreateImageWizardV2/steps/Oscap/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Form, Text, Title } from '@patternfly/react-core';
|
||||
|
||||
import { Oscap } from './Oscap';
|
||||
|
||||
import { imageBuilderApi } from '../../../../store/enhancedImageBuilderApi';
|
||||
import { useAppSelector } from '../../../../store/hooks';
|
||||
import { selectDistribution } from '../../../../store/wizardSlice';
|
||||
import DocumentationButton from '../../../sharedComponents/DocumentationButton';
|
||||
|
||||
const OscapStep = () => {
|
||||
const prefetchOscapProfile = imageBuilderApi.usePrefetch('getOscapProfiles');
|
||||
const release = useAppSelector((state) => selectDistribution(state));
|
||||
prefetchOscapProfile({ distribution: release });
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel="h2">OpenSCAP profile</Title>
|
||||
<Text>
|
||||
Use OpenSCAP to monitor the adherence of your registered RHEL systems to
|
||||
a selected regulatory compliance profile
|
||||
<br />
|
||||
<DocumentationButton />
|
||||
</Text>
|
||||
<Oscap />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default OscapStep;
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { Distributions, ImageRequest, ImageTypes } from './imageBuilderApi';
|
||||
import {
|
||||
DistributionProfileItem,
|
||||
Distributions,
|
||||
ImageRequest,
|
||||
ImageTypes,
|
||||
} from './imageBuilderApi';
|
||||
import { ActivationKeys } from './rhsmApi';
|
||||
|
||||
import {
|
||||
|
|
@ -37,6 +42,9 @@ type wizardState = {
|
|||
registrationType: string;
|
||||
activationKey: ActivationKeys['name'];
|
||||
};
|
||||
openScap: {
|
||||
profile: DistributionProfileItem | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
const initialState: wizardState = {
|
||||
|
|
@ -61,6 +69,9 @@ const initialState: wizardState = {
|
|||
registrationType: 'register-now-rhc',
|
||||
activationKey: '',
|
||||
},
|
||||
openScap: {
|
||||
profile: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const selectServerUrl = (state: RootState) => {
|
||||
|
|
@ -117,6 +128,10 @@ export const selectActivationKey = (state: RootState) => {
|
|||
return state.wizard.registration.activationKey;
|
||||
};
|
||||
|
||||
export const selectProfile = (state: RootState) => {
|
||||
return state.wizard.openScap.profile;
|
||||
};
|
||||
|
||||
export const wizardSlice = createSlice({
|
||||
name: 'wizard',
|
||||
initialState,
|
||||
|
|
@ -190,6 +205,12 @@ export const wizardSlice = createSlice({
|
|||
) => {
|
||||
state.registration.activationKey = action.payload;
|
||||
},
|
||||
changeOscapProfile: (
|
||||
state,
|
||||
action: PayloadAction<DistributionProfileItem | undefined>
|
||||
) => {
|
||||
state.openScap.profile = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -210,5 +231,6 @@ export const {
|
|||
changeGcpEmail,
|
||||
changeRegistrationType,
|
||||
changeActivationKey,
|
||||
changeOscapProfile,
|
||||
} = wizardSlice.actions;
|
||||
export default wizardSlice.reducer;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
import React from 'react';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import CreateImageWizard from '../../../Components/CreateImageWizardV2/CreateImageWizard';
|
||||
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
|
||||
import { clickNext, renderCustomRoutesWithReduxRouter } from '../../testUtils';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: 'insights/image-builder/*',
|
||||
element: <div />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/imagewizard/:composeId?',
|
||||
element: <CreateImageWizard />,
|
||||
},
|
||||
{
|
||||
path: 'insights/image-builder/share/:composeId',
|
||||
element: <ShareImageModal />,
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
auth: {
|
||||
getUser: () => {
|
||||
return {
|
||||
identity: {
|
||||
internal: {
|
||||
org_id: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
isBeta: () => true,
|
||||
isProd: () => false,
|
||||
getEnvironment: () => 'stage',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@unleash/proxy-client-react', () => ({
|
||||
useUnleashContext: () => jest.fn(),
|
||||
useFlag: jest.fn((flag) =>
|
||||
flag === 'image-builder.wizard.oscap.enabled' ? true : false
|
||||
),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
// scrollTo is not defined in jsdom
|
||||
window.HTMLElement.prototype.scrollTo = function () {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Step Compliance', () => {
|
||||
const user = userEvent.setup();
|
||||
const setup = async () => {
|
||||
renderCustomRoutesWithReduxRouter('imagewizard', {}, routes);
|
||||
};
|
||||
test('create an image with None oscap profile', async () => {
|
||||
await setup();
|
||||
|
||||
// select aws as upload destination
|
||||
await user.click(await screen.findByTestId('upload-aws'));
|
||||
await clickNext();
|
||||
|
||||
// aws step
|
||||
await user.click(
|
||||
await screen.findByRole('radio', {
|
||||
name: /manually enter an account id\./i,
|
||||
})
|
||||
);
|
||||
await user.type(
|
||||
await screen.findByRole('textbox', {
|
||||
name: 'aws account id',
|
||||
}),
|
||||
'012345678901'
|
||||
);
|
||||
|
||||
await clickNext();
|
||||
// skip registration
|
||||
await user.click(await screen.findByLabelText('Register later'));
|
||||
await clickNext();
|
||||
|
||||
// Now we should be in the Compliance step
|
||||
await screen.findByRole('heading', { name: /OpenSCAP/i });
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('textbox', { name: /select a profile/i })
|
||||
);
|
||||
await user.click(await screen.findByText(/none/i));
|
||||
|
||||
// check that the FSC does not contain a /tmp partition
|
||||
await clickNext();
|
||||
// await screen.findByRole('heading', { name: /File system configuration/i });
|
||||
// expect(
|
||||
// screen.queryByRole('cell', {
|
||||
// name: /tmp/i,
|
||||
// })
|
||||
// ).not.toBeInTheDocument();
|
||||
|
||||
// check that there are no Packages contained when selecting the "None" profile option
|
||||
// await clickNext();
|
||||
// await screen.findByRole('heading', {
|
||||
// name: /Additional Red Hat packages/i,
|
||||
// });
|
||||
// await screen.findByText(/no packages added/i);
|
||||
});
|
||||
|
||||
test('create an image with an oscap profile', async () => {
|
||||
await setup();
|
||||
|
||||
// select aws as upload destination
|
||||
await user.click(await screen.findByTestId('upload-aws'));
|
||||
await clickNext();
|
||||
|
||||
// aws step
|
||||
await user.click(
|
||||
await screen.findByRole('radio', {
|
||||
name: /manually enter an account id\./i,
|
||||
})
|
||||
);
|
||||
|
||||
await user.type(
|
||||
await screen.findByRole('textbox', {
|
||||
name: 'aws account id',
|
||||
}),
|
||||
'012345678901'
|
||||
);
|
||||
|
||||
await clickNext();
|
||||
// skip registration
|
||||
await user.click(await screen.findByLabelText('Register later'));
|
||||
await clickNext();
|
||||
|
||||
// Now we should be at the OpenSCAP step
|
||||
await screen.findByRole('heading', { name: /OpenSCAP/i });
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('textbox', {
|
||||
name: /select a profile/i,
|
||||
})
|
||||
);
|
||||
|
||||
await user.click(
|
||||
await screen.findByText(
|
||||
/cis red hat enterprise linux 8 benchmark for level 1 - workstation/i
|
||||
)
|
||||
);
|
||||
// check that the FSC contains a /tmp partition
|
||||
await clickNext();
|
||||
// await screen.findByRole('heading', { name: /File system configuration/i });
|
||||
// await screen.findByText(/tmp/i);
|
||||
|
||||
// check that the Packages contain a nftable package
|
||||
// await clickNext();
|
||||
|
||||
// await screen.findByRole('heading', {
|
||||
// name: /Additional Red Hat packages/i,
|
||||
// });
|
||||
// await screen.findByText(/nftables/i);
|
||||
// await screen.findByText(/libselinux/i);
|
||||
});
|
||||
});
|
||||
//
|
||||
// TO DO - check if the correct version of Wizard is tested
|
||||
//
|
||||
//describe('On Recreate', () => {
|
||||
// const setup = async () => {
|
||||
// renderWithReduxRouter('imagewizard/1679d95b-8f1d-4982-8c53-8c2afa4ab04c');
|
||||
// };
|
||||
// test('with oscap profile', async () => {
|
||||
// const user = userEvent.setup();
|
||||
// await setup();
|
||||
// await screen.findByRole('button', {
|
||||
// name: /review/i,
|
||||
// });
|
||||
// const createImageButton = await screen.findByRole('button', {
|
||||
// name: /create image/i,
|
||||
// });
|
||||
// await waitFor(() => expect(createImageButton).toBeEnabled());
|
||||
|
||||
// check that the FSC contains a /tmp partition
|
||||
// There are two buttons with the same name but cannot easily select the DDF rendered sidenav.
|
||||
// The sidenav will be the first node found out of all buttons.
|
||||
// const buttonsFSC = await screen.findAllByRole('button', {
|
||||
// name: /file system configuration/i,
|
||||
// });
|
||||
// await user.click(buttonsFSC[0]);
|
||||
// await screen.findByRole('heading', { name: /file system configuration/i });
|
||||
// await screen.findByText('/tmp');
|
||||
|
||||
// check that the Packages contain a nftable package
|
||||
// await clickNext();
|
||||
// await screen.findByRole('heading', {
|
||||
// name: /Additional Red Hat packages/i,
|
||||
// });
|
||||
// await screen.findByText('nftables');
|
||||
// });
|
||||
//});
|
||||
Loading…
Add table
Add a link
Reference in a new issue