Wizard: Firewall ports input

This adds chipping input for ports on the Firewall step.
This commit is contained in:
regexowl 2025-01-15 09:54:04 +01:00 committed by Lucas Garfield
parent f0cf5b51d6
commit 4802d08214
6 changed files with 141 additions and 1 deletions

View file

@ -0,0 +1,32 @@
import React from 'react';
import { FormGroup } from '@patternfly/react-core';
import { useAppSelector } from '../../../../../store/hooks';
import {
addPort,
removePort,
selectPorts,
} from '../../../../../store/wizardSlice';
import ChippingInput from '../../../ChippingInput';
import { isPortValid } from '../../../validators';
const PortsInput = () => {
const ports = useAppSelector(selectPorts);
return (
<FormGroup label="Ports">
<ChippingInput
ariaLabel="Add ports"
placeholder="Add ports"
validator={isPortValid}
list={ports}
item="Port"
addAction={addPort}
removeAction={removePort}
/>
</FormGroup>
);
};
export default PortsInput;

View file

@ -2,6 +2,8 @@ import React from 'react';
import { Text, Form, Title } from '@patternfly/react-core'; import { Text, Form, Title } from '@patternfly/react-core';
import PortsInput from './components/PortsInput';
const FirewallStep = () => { const FirewallStep = () => {
return ( return (
<Form> <Form>
@ -9,6 +11,7 @@ const FirewallStep = () => {
Firewall Firewall
</Title> </Title>
<Text>Customize firewall settings for your image.</Text> <Text>Customize firewall settings for your image.</Text>
<PortsInput />
</Form> </Form>
); );
}; };

View file

@ -83,6 +83,7 @@ import {
selectHostname, selectHostname,
selectUsers, selectUsers,
selectMetadata, selectMetadata,
selectPorts,
} from '../../../store/wizardSlice'; } from '../../../store/wizardSlice';
import { FileSystemConfigurationType } from '../steps/FileSystem'; import { FileSystemConfigurationType } from '../steps/FileSystem';
import { import {
@ -323,6 +324,9 @@ function commonRequestToState(
ntpservers: request.customizations.timezone?.ntpservers || [], ntpservers: request.customizations.timezone?.ntpservers || [],
}, },
hostname: request.customizations.hostname || '', hostname: request.customizations.hostname || '',
firewall: {
ports: request.customizations.firewall?.ports || [],
},
}; };
} }
@ -523,7 +527,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
groups: undefined, groups: undefined,
timezone: getTimezone(state), timezone: getTimezone(state),
locale: getLocale(state), locale: getLocale(state),
firewall: undefined, firewall: getFirewall(state),
installation_device: undefined, installation_device: undefined,
fdo: undefined, fdo: undefined,
ignition: undefined, ignition: undefined,
@ -689,6 +693,18 @@ const getLocale = (state: RootState) => {
} }
}; };
const getFirewall = (state: RootState) => {
const ports = selectPorts(state);
if (ports.length === 0) {
return undefined;
} else {
return {
ports: ports,
};
}
};
const getCustomRepositories = (state: RootState) => { const getCustomRepositories = (state: RootState) => {
const customRepositories = selectCustomRepositories(state); const customRepositories = selectCustomRepositories(state);
const recommendedRepositories = selectRecommendedRepositories(state); const recommendedRepositories = selectRecommendedRepositories(state);

View file

@ -103,3 +103,7 @@ export const isHostnameValid = (hostname: string) => {
) )
); );
}; };
export const isPortValid = (port: string) => {
return /^(\d{1,5}|[a-z]{1,6})(-\d{1,5})?:[a-z]{1,6}$/.test(port);
};

View file

@ -136,6 +136,9 @@ export type wizardState = {
}; };
timezone: Timezone; timezone: Timezone;
hostname: string; hostname: string;
firewall: {
ports: string[];
};
metadata?: { metadata?: {
parent_id: string | null; parent_id: string | null;
exported_at: string; exported_at: string;
@ -216,6 +219,9 @@ export const initialState: wizardState = {
ntpservers: [], ntpservers: [],
}, },
hostname: '', hostname: '',
firewall: {
ports: [],
},
firstBoot: { script: '' }, firstBoot: { script: '' },
users: [], users: [],
}; };
@ -418,6 +424,10 @@ export const selectHostname = (state: RootState) => {
return state.wizard.hostname; return state.wizard.hostname;
}; };
export const selectPorts = (state: RootState) => {
return state.wizard.firewall.ports;
};
export const wizardSlice = createSlice({ export const wizardSlice = createSlice({
name: 'wizard', name: 'wizard',
initialState, initialState,
@ -838,6 +848,17 @@ export const wizardSlice = createSlice({
setUserSshKeyByIndex: (state, action: PayloadAction<UserSshKeyPayload>) => { setUserSshKeyByIndex: (state, action: PayloadAction<UserSshKeyPayload>) => {
state.users[action.payload.index].ssh_key = action.payload.sshKey; state.users[action.payload.index].ssh_key = action.payload.sshKey;
}, },
addPort: (state, action: PayloadAction<string>) => {
if (!state.firewall.ports.some((port) => port === action.payload)) {
state.firewall.ports.push(action.payload);
}
},
removePort: (state, action: PayloadAction<string>) => {
state.firewall.ports.splice(
state.firewall.ports.findIndex((port) => port === action.payload),
1
);
},
}, },
}); });
@ -906,6 +927,8 @@ export const {
addNtpServer, addNtpServer,
removeNtpServer, removeNtpServer,
changeHostname, changeHostname,
addPort,
removePort,
addUser, addUser,
setUserNameByIndex, setUserNameByIndex,
setUserPasswordByIndex, setUserPasswordByIndex,

View file

@ -2,9 +2,14 @@ import type { Router as RemixRouter } from '@remix-run/router';
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event'; import { userEvent } from '@testing-library/user-event';
import { CREATE_BLUEPRINT } from '../../../../../constants';
import { import {
blueprintRequest,
clickBack, clickBack,
clickNext, clickNext,
enterBlueprintName,
interceptBlueprintRequest,
openAndDismissSaveAndBuildModal,
verifyCancelButton, verifyCancelButton,
} from '../../wizardTestUtils'; } from '../../wizardTestUtils';
import { clickRegisterLater, renderCreateMode } from '../../wizardTestUtils'; import { clickRegisterLater, renderCreateMode } from '../../wizardTestUtils';
@ -32,6 +37,19 @@ const goToFirewallStep = async () => {
await clickNext(); // Firewall await clickNext(); // Firewall
}; };
const goToReviewStep = async () => {
await clickNext(); // First boot script
await clickNext(); // Details
await enterBlueprintName();
await clickNext(); // Review
};
const addPort = async (port: string) => {
const user = userEvent.setup();
const portsInput = await screen.findByPlaceholderText(/add port/i);
await waitFor(() => user.type(portsInput, port.concat(',')));
};
describe('Step Firewall', () => { describe('Step Firewall', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@ -59,6 +77,50 @@ describe('Step Firewall', () => {
await goToFirewallStep(); await goToFirewallStep();
await verifyCancelButton(router); await verifyCancelButton(router);
}); });
test('duplicate ports cannnot be added', async () => {
await renderCreateMode();
await goToFirewallStep();
expect(screen.queryByText('Port already exists.')).not.toBeInTheDocument();
await addPort('22:tcp');
await addPort('22:tcp');
await screen.findByText('Port already exists.');
});
test('port in an invalid format cannot be added', async () => {
await renderCreateMode();
await goToFirewallStep();
expect(screen.queryByText('Invalid format.')).not.toBeInTheDocument();
await addPort('00:wrongFormat');
await screen.findByText('Invalid format.');
});
});
describe('Firewall request generated correctly', () => {
test('with ports added', async () => {
await renderCreateMode();
await goToFirewallStep();
await addPort('22:tcp');
await addPort('imap:tcp');
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: {
firewall: {
ports: ['22:tcp', 'imap:tcp'],
},
},
};
await waitFor(() => {
expect(receivedRequest).toEqual(expectedRequest);
});
});
}); });
// TO DO Step Firewall -> revisit step button on Review works // TO DO Step Firewall -> revisit step button on Review works