Wizard: Add inputs for disabled and enabled services
This adds inputs for disabled and enabled systemd services. New tests are also added.
This commit is contained in:
parent
3cd3aa0176
commit
1e0cf96457
4 changed files with 340 additions and 1 deletions
|
|
@ -0,0 +1,87 @@
|
|||
import React from 'react';
|
||||
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
|
||||
import { useAppSelector } from '../../../../../store/hooks';
|
||||
import { useGetOscapCustomizationsQuery } from '../../../../../store/imageBuilderApi';
|
||||
import {
|
||||
addDisabledService,
|
||||
addEnabledService,
|
||||
removeDisabledService,
|
||||
removeEnabledService,
|
||||
selectComplianceProfileID,
|
||||
selectDistribution,
|
||||
selectServices,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
import ChippingInput from '../../../ChippingInput';
|
||||
import { isServiceValid } from '../../../validators';
|
||||
|
||||
const ServicesInput = () => {
|
||||
const disabledServices = useAppSelector(selectServices).disabled;
|
||||
const maskedServices = useAppSelector(selectServices).masked;
|
||||
const enabledServices = useAppSelector(selectServices).enabled;
|
||||
|
||||
const release = useAppSelector(selectDistribution);
|
||||
const complianceProfileID = useAppSelector(selectComplianceProfileID);
|
||||
|
||||
const { data: oscapProfileInfo } = useGetOscapCustomizationsQuery(
|
||||
{
|
||||
distribution: release,
|
||||
// @ts-ignore if complianceProfileID is undefined the query is going to get skipped, so it's safe here to ignore the linter here
|
||||
profile: complianceProfileID,
|
||||
},
|
||||
{
|
||||
skip: !complianceProfileID,
|
||||
}
|
||||
);
|
||||
|
||||
const disabledAndMaskedRequiredByOpenSCAP = disabledServices
|
||||
.concat(maskedServices)
|
||||
.filter(
|
||||
(service) =>
|
||||
oscapProfileInfo?.services?.disabled?.includes(service) ||
|
||||
oscapProfileInfo?.services?.masked?.includes(service)
|
||||
);
|
||||
|
||||
const enabledRequiredByOpenSCAP = enabledServices.filter((service) =>
|
||||
oscapProfileInfo?.services?.enabled?.includes(service)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup isRequired={false} label="Disabled services">
|
||||
<ChippingInput
|
||||
ariaLabel="Add disabled service"
|
||||
placeholder="Add disabled service"
|
||||
validator={isServiceValid}
|
||||
list={disabledServices
|
||||
.concat(maskedServices)
|
||||
.filter(
|
||||
(service) =>
|
||||
!disabledAndMaskedRequiredByOpenSCAP.includes(service)
|
||||
)}
|
||||
requiredList={disabledAndMaskedRequiredByOpenSCAP}
|
||||
item="Disabled service"
|
||||
addAction={addDisabledService}
|
||||
removeAction={removeDisabledService}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup isRequired={false} label="Enabled services">
|
||||
<ChippingInput
|
||||
ariaLabel="Add enabled service"
|
||||
placeholder="Add enabled service"
|
||||
validator={isServiceValid}
|
||||
list={enabledServices.filter(
|
||||
(service) => !enabledRequiredByOpenSCAP.includes(service)
|
||||
)}
|
||||
requiredList={enabledRequiredByOpenSCAP}
|
||||
item="Enabled service"
|
||||
addAction={addEnabledService}
|
||||
removeAction={removeEnabledService}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicesInput;
|
||||
|
|
@ -2,6 +2,8 @@ import React from 'react';
|
|||
|
||||
import { Text, Form, Title } from '@patternfly/react-core';
|
||||
|
||||
import ServicesInput from './components/ServicesInputs';
|
||||
|
||||
const ServicesStep = () => {
|
||||
return (
|
||||
<Form>
|
||||
|
|
@ -9,6 +11,7 @@ const ServicesStep = () => {
|
|||
Systemd services
|
||||
</Title>
|
||||
<Text>Enable and disable systemd services.</Text>
|
||||
<ServicesInput />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -816,12 +816,57 @@ export const wizardSlice = createSlice({
|
|||
changeEnabledServices: (state, action: PayloadAction<string[]>) => {
|
||||
state.services.enabled = action.payload;
|
||||
},
|
||||
addEnabledService: (state, action: PayloadAction<string>) => {
|
||||
if (
|
||||
!state.services.enabled.some((service) => service === action.payload)
|
||||
) {
|
||||
state.services.enabled.push(action.payload);
|
||||
}
|
||||
},
|
||||
removeEnabledService: (state, action: PayloadAction<string>) => {
|
||||
state.services.enabled.splice(
|
||||
state.services.enabled.findIndex(
|
||||
(service) => service === action.payload
|
||||
),
|
||||
1
|
||||
);
|
||||
},
|
||||
changeMaskedServices: (state, action: PayloadAction<string[]>) => {
|
||||
state.services.masked = action.payload;
|
||||
},
|
||||
addMaskedService: (state, action: PayloadAction<string>) => {
|
||||
if (
|
||||
!state.services.masked.some((service) => service === action.payload)
|
||||
) {
|
||||
state.services.masked.push(action.payload);
|
||||
}
|
||||
},
|
||||
removeMaskedService: (state, action: PayloadAction<string>) => {
|
||||
state.services.masked.splice(
|
||||
state.services.masked.findIndex(
|
||||
(service) => service === action.payload
|
||||
),
|
||||
1
|
||||
);
|
||||
},
|
||||
changeDisabledServices: (state, action: PayloadAction<string[]>) => {
|
||||
state.services.disabled = action.payload;
|
||||
},
|
||||
addDisabledService: (state, action: PayloadAction<string>) => {
|
||||
if (
|
||||
!state.services.disabled.some((service) => service === action.payload)
|
||||
) {
|
||||
state.services.disabled.push(action.payload);
|
||||
}
|
||||
},
|
||||
removeDisabledService: (state, action: PayloadAction<string>) => {
|
||||
state.services.disabled.splice(
|
||||
state.services.disabled.findIndex(
|
||||
(service) => service === action.payload
|
||||
),
|
||||
1
|
||||
);
|
||||
},
|
||||
changeKernelName: (state, action: PayloadAction<string>) => {
|
||||
state.kernel.name = action.payload;
|
||||
},
|
||||
|
|
@ -1014,8 +1059,14 @@ export const {
|
|||
loadWizardState,
|
||||
setFirstBootScript,
|
||||
changeEnabledServices,
|
||||
addEnabledService,
|
||||
removeEnabledService,
|
||||
changeMaskedServices,
|
||||
addMaskedService,
|
||||
removeMaskedService,
|
||||
changeDisabledServices,
|
||||
addDisabledService,
|
||||
removeDisabledService,
|
||||
changeKernelName,
|
||||
addKernelArg,
|
||||
removeKernelArg,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@ import type { Router as RemixRouter } from '@remix-run/router';
|
|||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
import { CREATE_BLUEPRINT } from '../../../../../constants';
|
||||
import {
|
||||
blueprintRequest,
|
||||
clickBack,
|
||||
clickNext,
|
||||
enterBlueprintName,
|
||||
interceptBlueprintRequest,
|
||||
openAndDismissSaveAndBuildModal,
|
||||
verifyCancelButton,
|
||||
} from '../../wizardTestUtils';
|
||||
import { clickRegisterLater, renderCreateMode } from '../../wizardTestUtils';
|
||||
|
|
@ -33,6 +38,75 @@ const goToServicesStep = async () => {
|
|||
await clickNext(); // Services
|
||||
};
|
||||
|
||||
const goToOpenSCAPStep = async () => {
|
||||
const user = userEvent.setup();
|
||||
const guestImageCheckBox = await screen.findByRole('checkbox', {
|
||||
name: /virtualization guest image checkbox/i,
|
||||
});
|
||||
await waitFor(() => user.click(guestImageCheckBox));
|
||||
await clickNext(); // Registration
|
||||
await clickRegisterLater();
|
||||
await clickNext(); // OpenSCAP
|
||||
};
|
||||
|
||||
const goFromOpenSCAPToServices = async () => {
|
||||
await clickNext(); // File system configuration
|
||||
await clickNext(); // Snapshots
|
||||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // Users
|
||||
await clickNext(); // Timezone
|
||||
await clickNext(); // Locale
|
||||
await clickNext(); // Hostname
|
||||
await clickNext(); // Kernel
|
||||
await clickNext(); // Firewall
|
||||
await clickNext(); // Services
|
||||
};
|
||||
|
||||
const goToReviewStep = async () => {
|
||||
await clickNext(); // First boot script
|
||||
await clickNext(); // Details
|
||||
await enterBlueprintName();
|
||||
await clickNext(); // Review
|
||||
};
|
||||
|
||||
const addDisabledService = async (service: string) => {
|
||||
const user = userEvent.setup();
|
||||
const disabledServiceInput = await screen.findByPlaceholderText(
|
||||
'Add disabled service'
|
||||
);
|
||||
await waitFor(() => user.type(disabledServiceInput, service.concat(' ')));
|
||||
};
|
||||
|
||||
const addEnabledService = async (service: string) => {
|
||||
const user = userEvent.setup();
|
||||
const enabledServiceInput = await screen.findByPlaceholderText(
|
||||
'Add enabled service'
|
||||
);
|
||||
await waitFor(() => user.type(enabledServiceInput, service.concat(' ')));
|
||||
};
|
||||
|
||||
const removeService = async (service: string) => {
|
||||
const user = userEvent.setup();
|
||||
const removeServiceButton = await screen.findByRole('button', {
|
||||
name: `close ${service}`,
|
||||
});
|
||||
await waitFor(() => user.click(removeServiceButton));
|
||||
};
|
||||
|
||||
const selectProfile = async () => {
|
||||
const user = userEvent.setup();
|
||||
const selectProfileDropdown = await screen.findByRole('textbox', {
|
||||
name: /select a profile/i,
|
||||
});
|
||||
await waitFor(() => user.click(selectProfileDropdown));
|
||||
|
||||
const cis1Profile = await screen.findByText(
|
||||
/CIS Red Hat Enterprise Linux 8 Benchmark for Level 1 - Workstation/i
|
||||
);
|
||||
await waitFor(() => user.click(cis1Profile));
|
||||
};
|
||||
|
||||
describe('Step Services', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -60,8 +134,132 @@ describe('Step Services', () => {
|
|||
await goToServicesStep();
|
||||
await verifyCancelButton(router);
|
||||
});
|
||||
|
||||
test('services can be added and removed', async () => {
|
||||
await renderCreateMode();
|
||||
await goToServicesStep();
|
||||
await addDisabledService('telnet');
|
||||
await addDisabledService('https');
|
||||
await removeService('telnet');
|
||||
expect(screen.queryByText('telnet')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('services from OpenSCAP get added correctly and cannot be removed', async () => {
|
||||
await renderCreateMode();
|
||||
await goToOpenSCAPStep();
|
||||
await selectProfile();
|
||||
await goFromOpenSCAPToServices();
|
||||
await screen.findAllByText('Required by OpenSCAP');
|
||||
// disabled services
|
||||
await screen.findByText('nfs-server');
|
||||
await screen.findByText('emacs-service');
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /close nfs-server/i })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /close emacs-service/i })
|
||||
).not.toBeInTheDocument();
|
||||
// enabled services
|
||||
await screen.findByText('crond');
|
||||
await screen.findByText('neovim-service');
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /close crond/i })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /close neovim-service/i })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Services request generated correctly', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('with services', async () => {
|
||||
await renderCreateMode();
|
||||
await goToServicesStep();
|
||||
await addDisabledService('telnet');
|
||||
await addEnabledService('httpd');
|
||||
await goToReviewStep();
|
||||
// informational modal pops up in the first test only as it's tied
|
||||
// to a 'imageBuilder.saveAndBuildModalSeen' variable in localStorage
|
||||
await openAndDismissSaveAndBuildModal();
|
||||
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
|
||||
|
||||
const expectedRequest = {
|
||||
...blueprintRequest,
|
||||
customizations: {
|
||||
services: {
|
||||
disabled: ['telnet'],
|
||||
enabled: ['httpd'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
|
||||
test('with added and removed services', async () => {
|
||||
await renderCreateMode();
|
||||
await goToServicesStep();
|
||||
await addDisabledService('telnet');
|
||||
await addEnabledService('httpd');
|
||||
await removeService('telnet');
|
||||
await removeService('httpd');
|
||||
await goToReviewStep();
|
||||
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
|
||||
|
||||
const expectedRequest = {
|
||||
...blueprintRequest,
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
|
||||
test('with OpenSCAP profile that includes services', async () => {
|
||||
await renderCreateMode();
|
||||
await goToOpenSCAPStep();
|
||||
await selectProfile();
|
||||
await goFromOpenSCAPToServices();
|
||||
await goToReviewStep();
|
||||
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
|
||||
|
||||
const expectedRequest = {
|
||||
...blueprintRequest,
|
||||
customizations: {
|
||||
filesystem: [
|
||||
{
|
||||
min_size: 10737418240,
|
||||
mountpoint: '/',
|
||||
},
|
||||
{ min_size: 1073741824, mountpoint: '/tmp' },
|
||||
{ min_size: 1073741824, mountpoint: '/home' },
|
||||
],
|
||||
openscap: {
|
||||
profile_id: 'xccdf_org.ssgproject.content_profile_cis_workstation_l1',
|
||||
},
|
||||
packages: ['aide', 'neovim'],
|
||||
kernel: {
|
||||
append: 'audit_backlog_limit=8192 audit=1',
|
||||
},
|
||||
services: {
|
||||
masked: ['nfs-server', 'emacs-service'],
|
||||
disabled: ['rpcbind', 'autofs', 'nftables'],
|
||||
enabled: ['crond', 'neovim-service'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TO DO 'Services step' -> 'revisit step button on Review works'
|
||||
// TO DO 'Services request generated correctly'
|
||||
// TO DO 'Services edit mode'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue