Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upcoming: [M3-9141] - Add udp_check_port support to NodeBalancers #11534

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 @@ -114,49 +116,58 @@ describe('create NodeBalancer', () => {
* - Confirms label field displays error if it contains special characters.
* - 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,
});
it.only('displays API errors for NodeBalancer Create form fields', () => {
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
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
Loading