Wizard: Add masked services input

This exposes masked services in the UI by adding a masked services input.
This commit is contained in:
regexowl 2025-02-26 16:08:42 +01:00 committed by Lucas Garfield
parent 408ecb2a80
commit 56e85e0954
6 changed files with 105 additions and 20 deletions

View file

@ -458,7 +458,9 @@ const Review = () => {
</ExpandableSection>
)}
{isServicesStepEnabled &&
(services.enabled.length > 0 || services.disabled.length > 0) && (
(services.enabled.length > 0 ||
services.disabled.length > 0 ||
services.masked.length > 0) && (
<ExpandableSection
toggleContent={composeExpandable(
'Systemd services',

View file

@ -991,11 +991,24 @@ export const ServicesList = () => {
Disabled
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{services.disabled.length > 0 || services.masked.length > 0 ? (
{services.disabled.length > 0 ? (
<CodeBlock>
<CodeBlockCode>
{services.disabled.concat(services.masked).join(' ')}
</CodeBlockCode>
<CodeBlockCode>{services.disabled.join(' ')}</CodeBlockCode>
</CodeBlock>
) : (
'None'
)}
</TextListItem>
<TextListItem
component={TextListItemVariants.dt}
className="pf-v5-u-min-width"
>
Masked
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{services.masked.length > 0 ? (
<CodeBlock>
<CodeBlockCode>{services.masked.join(' ')}</CodeBlockCode>
</CodeBlock>
) : (
'None'

View file

@ -7,8 +7,10 @@ import { useGetOscapCustomizationsQuery } from '../../../../../store/imageBuilde
import {
addDisabledService,
addEnabledService,
addMaskedService,
removeDisabledService,
removeEnabledService,
removeMaskedService,
selectComplianceProfileID,
selectDistribution,
selectServices,
@ -38,13 +40,13 @@ const ServicesInput = () => {
}
);
const disabledAndMaskedRequiredByOpenSCAP = disabledServices
.concat(maskedServices)
.filter(
(service) =>
oscapProfileInfo?.services?.disabled?.includes(service) ||
oscapProfileInfo?.services?.masked?.includes(service)
);
const disabledRequiredByOpenSCAP = disabledServices.filter((service) =>
oscapProfileInfo?.services?.disabled?.includes(service)
);
const maskedRequiredByOpenSCAP = maskedServices.filter((service) =>
oscapProfileInfo?.services?.masked?.includes(service)
);
const enabledRequiredByOpenSCAP = enabledServices.filter((service) =>
oscapProfileInfo?.services?.enabled?.includes(service)
@ -57,13 +59,11 @@ const ServicesInput = () => {
ariaLabel="Add disabled service"
placeholder="Add disabled service"
validator={isServiceValid}
list={disabledServices
.concat(maskedServices)
.filter(
(service) =>
!disabledAndMaskedRequiredByOpenSCAP.includes(service)
)}
requiredList={disabledAndMaskedRequiredByOpenSCAP}
list={disabledServices.filter(
(service) =>
!oscapProfileInfo?.services?.disabled?.includes(service)
)}
requiredList={disabledRequiredByOpenSCAP}
item="Disabled service"
addAction={addDisabledService}
removeAction={removeDisabledService}
@ -71,6 +71,22 @@ const ServicesInput = () => {
fieldName="disabledSystemdServices"
/>
</FormGroup>
<FormGroup isRequired={false} label="Masked services">
<ChippingInput
ariaLabel="Add masked service"
placeholder="Add masked service"
validator={isServiceValid}
list={maskedServices.filter(
(service) => !oscapProfileInfo?.services?.masked?.includes(service)
)}
requiredList={maskedRequiredByOpenSCAP}
item="Masked service"
addAction={addMaskedService}
removeAction={removeMaskedService}
stepValidation={stepValidation}
fieldName="maskedSystemdServices"
/>
</FormGroup>
<FormGroup isRequired={false} label="Enabled services">
<ChippingInput
ariaLabel="Add enabled service"

View file

@ -10,7 +10,7 @@ const ServicesStep = () => {
<Title headingLevel="h1" size="xl">
Systemd services
</Title>
<Text>Enable and disable systemd services.</Text>
<Text>Enable, disable and mask systemd services.</Text>
<ServicesInput />
</Form>
);

View file

@ -341,6 +341,7 @@ export function useServicesValidation(): StepValidation {
const services = useAppSelector(selectServices);
const errors = {};
const invalidDisabled = [];
const invalidMasked = [];
const invalidEnabled = [];
if (services.disabled.length > 0) {
@ -357,6 +358,20 @@ export function useServicesValidation(): StepValidation {
}
}
if (services.masked.length > 0) {
for (const s of services.masked) {
if (!isServiceValid(s)) {
invalidMasked.push(s);
}
}
if (invalidMasked.length > 0) {
Object.assign(errors, {
maskedSystemdServices: `Invalid masked services: ${invalidMasked}`,
});
}
}
if (services.enabled.length > 0) {
for (const s of services.enabled) {
if (!isServiceValid(s)) {

View file

@ -82,6 +82,14 @@ const addDisabledService = async (service: string) => {
await waitFor(() => user.type(disabledServiceInput, service.concat(' ')));
};
const addMaskedService = async (service: string) => {
const user = userEvent.setup();
const maskedServiceInput = await screen.findByPlaceholderText(
'Add masked service'
);
await waitFor(() => user.type(maskedServiceInput, service.concat(' ')));
};
const addEnabledService = async (service: string) => {
const user = userEvent.setup();
const enabledServiceInput = await screen.findByPlaceholderText(
@ -157,6 +165,33 @@ describe('Step Services', () => {
expect(screen.queryByText('telnet')).not.toBeInTheDocument();
});
test('validation works', async () => {
const user = userEvent.setup();
await renderCreateMode();
await goToServicesStep();
const clearInputButtons = await screen.findAllByRole('button', {
name: /clear input/i,
});
// Disabled services input
expect(screen.queryByText('Invalid format.')).not.toBeInTheDocument();
await addDisabledService('-------');
expect(await screen.findByText('Invalid format.')).toBeInTheDocument();
await waitFor(() => user.click(clearInputButtons[0]));
// Masked services input
expect(screen.queryByText('Invalid format.')).not.toBeInTheDocument();
await addMaskedService('-------');
expect(await screen.findByText('Invalid format.')).toBeInTheDocument();
await waitFor(() => user.click(clearInputButtons[1]));
// Enabled services input
expect(screen.queryByText('Invalid format.')).not.toBeInTheDocument();
await addEnabledService('-------');
expect(await screen.findByText('Invalid format.')).toBeInTheDocument();
await waitFor(() => user.click(clearInputButtons[2]));
});
test('services from OpenSCAP get added correctly and cannot be removed', async () => {
await renderCreateMode();
await goToOpenSCAPStep();
@ -202,6 +237,7 @@ describe('Services request generated correctly', () => {
await renderCreateMode();
await goToServicesStep();
await addDisabledService('telnet');
await addMaskedService('nfs-server');
await addEnabledService('httpd');
await goToReviewStep();
// informational modal pops up in the first test only as it's tied
@ -214,6 +250,7 @@ describe('Services request generated correctly', () => {
customizations: {
services: {
disabled: ['telnet'],
masked: ['nfs-server'],
enabled: ['httpd'],
},
},
@ -228,8 +265,10 @@ describe('Services request generated correctly', () => {
await renderCreateMode();
await goToServicesStep();
await addDisabledService('telnet');
await addMaskedService('nfs-server');
await addEnabledService('httpd');
await removeService('telnet');
await removeService('nfs-server');
await removeService('httpd');
await goToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);