Skip to content

Commit

Permalink
upcoming: [M3-9141] - Add udp_check_port support to NodeBalancers (l…
Browse files Browse the repository at this point in the history
…inode#11534)

* add `udp_check_port` support

* improve the ui shifting

* add changesets

* fix unit test

* fix prettier

* fix cypress test now that Cookie option was removed

* fix typecheck

* remove test skip

* make payload satisfy api change

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
  • Loading branch information
bnussman-akamai and bnussman authored Jan 24, 2025
1 parent f01831b commit cc51ff4
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 150 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11534-changed-1737137202046.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

Don't allow "HTTP Cookie" session stickiness when NodeBalancer config protocol is TCP ([#11534](https://github.com/linode/manager/pull/11534))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add support for NodeBalancer UDP Health Check Port ([#11534](https://github.com/linode/manager/pull/11534))
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ const deployNodeBalancer = () => {
cy.get('[data-qa-deploy-nodebalancer]').click();
};

import { nodeBalancerFactory } from 'src/factories';
import { linodeFactory, nodeBalancerFactory, regionFactory } from 'src/factories';
import { interceptCreateNodeBalancer } from 'support/intercepts/nodebalancers';
import { mockGetRegions } from 'support/intercepts/regions';
import { mockGetLinodes } from 'support/intercepts/linodes';

const createNodeBalancerWithUI = (
nodeBal: NodeBalancer,
Expand Down Expand Up @@ -115,48 +117,57 @@ describe('create NodeBalancer', () => {
* - Confirms session stickiness field displays error if protocol is not HTTP or HTTPS.
*/
it('displays API errors for NodeBalancer Create form fields', () => {
const region = chooseRegion();
const linodePayload = {
region: region.id,
// NodeBalancers require Linodes with private IPs.
private_ip: true,
};
cy.defer(() => createTestLinode(linodePayload)).then((linode) => {
const nodeBal = nodeBalancerFactory.build({
label: `${randomLabel()}-^`,
ipv4: linode.ipv4[1],
region: region.id,
});
const region = regionFactory.build({ capabilities: ['NodeBalancers'] });
const linode = linodeFactory.build({ ipv4: ['192.168.1.213'] });

// catch request
interceptCreateNodeBalancer().as('createNodeBalancer');
mockGetRegions([region]);
mockGetLinodes([linode]);
interceptCreateNodeBalancer().as('createNodeBalancer')

createNodeBalancerWithUI(nodeBal);
cy.findByText(`Label can't contain special characters or spaces.`).should(
'be.visible'
);
cy.get('[id="nodebalancer-label"]')
.should('be.visible')
.click()
.clear()
.type(randomLabel());

cy.get('[data-qa-protocol-select="true"]').click().type('TCP{enter}');

cy.get('[data-qa-session-stickiness-select]')
.click()
.type('HTTP Cookie{enter}');

deployNodeBalancer();
const errMessage = `Stickiness http_cookie requires protocol 'http' or 'https'`;
cy.wait('@createNodeBalancer')
.its('response.body')
.should('deep.equal', {
errors: [{ field: 'configs[0].stickiness', reason: errMessage }],
});
cy.visitWithLogin('/nodebalancers/create');

cy.findByText(errMessage).should('be.visible');
});
cy.findByLabelText('NodeBalancer Label')
.should('be.visible')
.type('my-nodebalancer-1');

ui.autocomplete.findByLabel('Region')
.should('be.visible')
.click();

ui.autocompletePopper.findByTitle(region.id, { exact: false })
.should('be.visible')
.should('be.enabled')
.click();

cy.findByLabelText('Label')
.type("my-node-1");

cy.findByLabelText('IP Address')
.click()
.type(linode.ipv4[0]);

ui.autocompletePopper.findByTitle(linode.label)
.click();

ui.button.findByTitle('Create NodeBalancer')
.scrollIntoView()
.should('be.enabled')
.should('be.visible')
.click();

const expectedError = 'Address Restricted: IP must not be within 192.168.0.0/17';

cy.wait('@createNodeBalancer')
.its('response.body')
.should('deep.equal', {
errors: [
{ field: 'region', reason: 'region is not valid' },
{ field: 'configs[0].nodes[0].address', reason: expectedError }
],
});

cy.findByText(expectedError)
.should('be.visible');
});

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import Grid from '@mui/material/Unstable_Grid2';
import * as React from 'react';

import { useFlags } from 'src/hooks/useFlags';

import { setErrorMap } from './utils';

import type { NodeBalancerConfigPanelProps } from './types';
Expand All @@ -32,6 +34,7 @@ const displayProtocolText = (p: string) => {
};

export const ActiveCheck = (props: ActiveCheckProps) => {
const flags = useFlags();
const {
checkBody,
checkPath,
Expand All @@ -44,6 +47,7 @@ export const ActiveCheck = (props: ActiveCheckProps) => {
healthCheckTimeout,
healthCheckType,
protocol,
udpCheckPort,
} = props;

const errorMap = setErrorMap(errors || []);
Expand Down Expand Up @@ -94,7 +98,7 @@ export const ActiveCheck = (props: ActiveCheckProps) => {

return (
<Grid md={6} xs={12}>
<Grid container spacing={2} sx={{ padding: 1 }}>
<Grid container spacing={1} sx={{ padding: 1 }}>
<Grid xs={12}>
<Typography data-qa-active-checks-header variant="h2">
Active Health Checks
Expand Down Expand Up @@ -129,7 +133,50 @@ export const ActiveCheck = (props: ActiveCheckProps) => {
</Grid>
{healthCheckType !== 'none' && (
<Grid container>
<Grid xs={12}>
{['http', 'http_body'].includes(healthCheckType) && (
<Grid xs={12}>
<TextField
data-testid="http-path"
disabled={disabled}
errorGroup={forEdit ? `${configIdx}` : undefined}
errorText={errorMap.check_path}
label="Check HTTP Path"
onChange={onCheckPathChange}
required={['http', 'http_body'].includes(healthCheckType)}
value={checkPath || ''}
/>
</Grid>
)}
{healthCheckType === 'http_body' && (
<Grid md={12} xs={12}>
<TextField
data-testid="http-body"
disabled={disabled}
errorGroup={forEdit ? `${configIdx}` : undefined}
errorText={errorMap.check_body}
label="Expected HTTP Body"
onChange={onCheckBodyChange}
required={healthCheckType === 'http_body'}
value={checkBody}
/>
</Grid>
)}
{flags.udp && protocol === 'udp' && (
<Grid lg={6}>
<TextField
disabled={disabled}
errorGroup={forEdit ? `${configIdx}` : undefined}
errorText={errorMap.udp_check_port}
label="Health Check Port"
max={65535}
min={1}
onChange={(e) => props.onUdpCheckPortChange(+e.target.value)}
type="number"
value={udpCheckPort}
/>
</Grid>
)}
<Grid lg={6} xs={12}>
<TextField
InputProps={{
'aria-label': 'Active Health Check Interval',
Expand All @@ -152,7 +199,7 @@ export const ActiveCheck = (props: ActiveCheckProps) => {
Seconds between health check probes
</FormHelperText>
</Grid>
<Grid xs={12}>
<Grid lg={6} xs={12}>
<TextField
InputProps={{
'aria-label': 'Active Health Check Timeout',
Expand Down Expand Up @@ -197,34 +244,6 @@ export const ActiveCheck = (props: ActiveCheckProps) => {
1-30
</FormHelperText>
</Grid>
{['http', 'http_body'].includes(healthCheckType) && (
<Grid lg={6} xs={12}>
<TextField
data-testid="http-path"
disabled={disabled}
errorGroup={forEdit ? `${configIdx}` : undefined}
errorText={errorMap.check_path}
label="Check HTTP Path"
onChange={onCheckPathChange}
required={['http', 'http_body'].includes(healthCheckType)}
value={checkPath || ''}
/>
</Grid>
)}
{healthCheckType === 'http_body' && (
<Grid md={12} xs={12}>
<TextField
data-testid="http-body"
disabled={disabled}
errorGroup={forEdit ? `${configIdx}` : undefined}
errorText={errorMap.check_body}
label="Expected HTTP Body"
onChange={onCheckBodyChange}
required={healthCheckType === 'http_body'}
value={checkBody}
/>
</Grid>
)}
</Grid>
)}
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ export const nbConfigPanelMockPropsForTest: NodeBalancerConfigPanelProps = {
onSave: vi.fn(),
onSessionStickinessChange: vi.fn(),
onSslCertificateChange: vi.fn(),
onUdpCheckPortChange: vi.fn(),
port: 80,
privateKey: '',
protocol: 'http',
proxyProtocol: 'none',
removeNode: vi.fn(),
sessionStickiness: 'table',
sslCertificate: '',
udpCheckPort: 80,
};

const activeHealthChecksFormInputs = ['Interval', 'Timeout', 'Attempts'];
Expand Down Expand Up @@ -368,4 +370,26 @@ describe('NodeBalancerConfigPanel', () => {
expect(getByText(algorithm)).toBeVisible();
}
});

it('shows a "Health Check Port" field when health checks are enabled', async () => {
const onChange = vi.fn();

const { getByLabelText } = renderWithTheme(
<NodeBalancerConfigPanel
{...nbConfigPanelMockPropsForTest}
healthCheckType="connection"
onUdpCheckPortChange={onChange}
protocol="udp"
/>,
{ flags: { udp: true } }
);

const checkPortField = getByLabelText('Health Check Port');

expect(checkPortField).toBeVisible();

await userEvent.type(checkPortField, '8080');

expect(onChange).toHaveBeenCalledWith(8080);
});
});
Loading

0 comments on commit cc51ff4

Please sign in to comment.