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-8870] - Begin adding UDP Support for NodeBalancers #11405

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Initial support for NodeBalancer UDP protocol ([#11405](https://github.com/linode/manager/pull/11405))
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@ export const SelectFirewallPanel = (props: Props) => {
: null;

return (
<Paper
data-testid="select-firewall-panel"
sx={(theme) => ({ marginTop: theme.spacing(3) })}
>
<Paper data-testid="select-firewall-panel">
<Typography
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
variant="h2"
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export interface Flags {
taxId: BaseFeatureFlag;
taxes: Taxes;
tpaProviders: Provider[];
udp: boolean;
}

interface MarketplaceAppOverride {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const node = {
const props: NodeBalancerConfigNodeProps = {
configIdx: 1,
disabled: false,
forEdit: true,
disallowRemoval: false,
hideModeSelect: false,
idx: 1,
node,
onNodeAddressChange: vi.fn(),
Expand Down Expand Up @@ -49,17 +50,17 @@ describe('NodeBalancerConfigNode', () => {
expect(getByText('DOWN')).toBeVisible();
});

it('cannot change the mode if the node is not for edit', () => {
it('hides the "Mode" select if hideModeSelect is true', () => {
const { queryByText } = renderWithTheme(
<NodeBalancerConfigNode {...props} forEdit={false} />
<NodeBalancerConfigNode {...props} hideModeSelect />
);

expect(queryByText('Mode')).not.toBeInTheDocument();
});

it('cannot remove the node if the node is not for edit or is the first node', () => {
it('cannot remove the node if the disallowRemoval is true', () => {
const { queryByText } = renderWithTheme(
<NodeBalancerConfigNode {...props} forEdit={false} idx={0} />
<NodeBalancerConfigNode {...props} disallowRemoval />
);

expect(queryByText('Remove')).not.toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import type { NodeBalancerConfigNodeFields } from './types';
import type { NodeBalancerConfigNodeMode } from '@linode/api-v4';

export interface NodeBalancerConfigNodeProps {
configIdx?: number;
configIdx: number;
disabled: boolean;
forEdit: boolean;
disallowRemoval: boolean;
hideModeSelect: boolean;
idx: number;
node: NodeBalancerConfigNodeFields;
nodeBalancerRegion?: string;
Expand Down Expand Up @@ -55,7 +56,8 @@ export const NodeBalancerConfigNode = React.memo(
const {
configIdx,
disabled,
forEdit,
disallowRemoval,
hideModeSelect,
idx,
node,
nodeBalancerRegion,
Expand Down Expand Up @@ -85,7 +87,7 @@ export const NodeBalancerConfigNode = React.memo(
<Divider
style={{
marginBottom: 24,
marginTop: forEdit ? 8 : 24,
marginTop: 8,
}}
/>
</Grid>
Expand All @@ -96,24 +98,16 @@ export const NodeBalancerConfigNode = React.memo(
</Grid>
)}
<Grid container spacing={2}>
<Grid
sx={{
'.MuiInputLabel-root': {
marginTop: 0,
},
}}
lg={forEdit ? 2 : 4}
sm={forEdit ? 4 : 6}
xs={12}
>
<Grid lg={4} sm={6} xs={12}>
<TextField
data-qa-backend-ip-label
disabled={disabled}
errorGroup={forEdit ? `${configIdx}` : undefined}
errorGroup={`${configIdx}`}
errorText={nodesErrorMap.label}
inputId={`node-label-${configIdx}-${idx}`}
inputProps={{ 'data-node-idx': idx }}
label="Label"
noMarginTop
onChange={onNodeLabelChange}
value={node.label}
/>
Expand All @@ -134,7 +128,7 @@ export const NodeBalancerConfigNode = React.memo(
</Grid>
<Grid sx={{ padding: 1 }} xs={12}>
<Grid container data-qa-node key={idx} spacing={2}>
<Grid lg={forEdit ? 2 : 4} sm={3} xs={12}>
<Grid lg={3} sm={4} xs={12}>
<ConfigNodeIPSelect
disabled={disabled}
errorText={nodesErrorMap.address}
Expand All @@ -149,7 +143,7 @@ export const NodeBalancerConfigNode = React.memo(
<TextField
data-qa-backend-ip-port
disabled={disabled}
errorGroup={forEdit ? `${configIdx}` : undefined}
errorGroup={`${configIdx}`}
errorText={nodesErrorMap.port}
inputProps={{ 'data-node-idx': idx }}
label="Port"
Expand All @@ -163,7 +157,7 @@ export const NodeBalancerConfigNode = React.memo(
<TextField
data-qa-backend-ip-weight
disabled={disabled}
errorGroup={forEdit ? `${configIdx}` : undefined}
errorGroup={`${configIdx}`}
errorText={nodesErrorMap.weight}
inputProps={{ 'data-node-idx': idx }}
label="Weight"
Expand All @@ -173,7 +167,7 @@ export const NodeBalancerConfigNode = React.memo(
value={node.weight}
/>
</Grid>
{forEdit && (
{!hideModeSelect && (
<Grid lg={2} sm={3} xs={6}>
<Autocomplete
value={modeOptions.find(
Expand All @@ -189,7 +183,7 @@ export const NodeBalancerConfigNode = React.memo(
/>
</Grid>
)}
{(forEdit || idx !== 0) && (
{!disallowRemoval && (
<Box alignSelf="flex-end" paddingBottom={1}>
<Button disabled={disabled} onClick={() => removeNode(idx)}>
Remove
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import * as React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import {
LEAST_CONNECTIONS_ALGORITHM_HELPER_TEXT,
NodeBalancerConfigPanel,
ROUND_ROBIN_ALGORITHM_HELPER_TEXT,
SOURCE_ALGORITHM_HELPER_TEXT,
} from './NodeBalancerConfigPanel';
import { ALGORITHM_HELPER_TEXT } from './constants';
import { NodeBalancerConfigPanel } from './NodeBalancerConfigPanel';

import type {
NodeBalancerConfigNodeFields,
Expand Down Expand Up @@ -222,45 +218,51 @@ describe('NodeBalancerConfigPanel', () => {

it('renders the relevant helper text for the Round Robin algorithm', () => {
const { getByText, queryByText } = renderWithTheme(
<NodeBalancerConfigPanel {...nbConfigPanelMockPropsForTest} />
<NodeBalancerConfigPanel
{...nbConfigPanelMockPropsForTest}
algorithm="roundrobin"
/>
);

expect(getByText(ROUND_ROBIN_ALGORITHM_HELPER_TEXT)).toBeVisible();
expect(getByText(ALGORITHM_HELPER_TEXT.roundrobin)).toBeVisible();

expect(queryByText(ALGORITHM_HELPER_TEXT.source)).not.toBeInTheDocument();
expect(
queryByText(LEAST_CONNECTIONS_ALGORITHM_HELPER_TEXT)
queryByText(ALGORITHM_HELPER_TEXT.leastconn)
).not.toBeInTheDocument();
expect(queryByText(SOURCE_ALGORITHM_HELPER_TEXT)).not.toBeInTheDocument();
});

it('renders the relevant helper text for the Least Connections algorithm', () => {
const { getByText, queryByText } = renderWithTheme(
<NodeBalancerConfigPanel
{...nbConfigPanelMockPropsForTest}
algorithm={'leastconn'}
algorithm="leastconn"
/>
);

expect(getByText(LEAST_CONNECTIONS_ALGORITHM_HELPER_TEXT)).toBeVisible();
expect(queryByText(SOURCE_ALGORITHM_HELPER_TEXT)).not.toBeInTheDocument();
expect(getByText(ALGORITHM_HELPER_TEXT.leastconn)).toBeVisible();

expect(queryByText(ALGORITHM_HELPER_TEXT.source)).not.toBeInTheDocument();
expect(
queryByText(ROUND_ROBIN_ALGORITHM_HELPER_TEXT)
queryByText(ALGORITHM_HELPER_TEXT.roundrobin)
).not.toBeInTheDocument();
});

it('renders the relevant helper text for the Source algorithm', () => {
const { getByText, queryByText } = renderWithTheme(
<NodeBalancerConfigPanel
{...nbConfigPanelMockPropsForTest}
algorithm={'source'}
algorithm="source"
/>
);

expect(getByText(SOURCE_ALGORITHM_HELPER_TEXT)).toBeVisible();
expect(getByText(ALGORITHM_HELPER_TEXT.source)).toBeVisible();

expect(
queryByText(ROUND_ROBIN_ALGORITHM_HELPER_TEXT)
queryByText(ALGORITHM_HELPER_TEXT.leastconn)
).not.toBeInTheDocument();
expect(
queryByText(LEAST_CONNECTIONS_ALGORITHM_HELPER_TEXT)
queryByText(ALGORITHM_HELPER_TEXT.roundrobin)
).not.toBeInTheDocument();
});

Expand Down Expand Up @@ -317,4 +319,53 @@ describe('NodeBalancerConfigPanel', () => {
await userEvent.click(editConfigButton);
expect(nbConfigPanelMockPropsForTest.onSave).toHaveBeenCalled();
});

it('does not show the passive checks option for the UDP protocol', () => {
const { queryByText } = renderWithTheme(
<NodeBalancerConfigPanel
{...nbConfigPanelMockPropsForTest}
protocol="udp"
/>
);

expect(queryByText('Passive Checks')).not.toBeInTheDocument();
});

it('shows correct algorithm options for the UDP protocol', async () => {
const { getByLabelText, getByText } = renderWithTheme(
<NodeBalancerConfigPanel
{...nbConfigPanelMockPropsForTest}
protocol="udp"
/>
);

const algorithmField = getByLabelText('Algorithm');

expect(algorithmField).toBeVisible();

await userEvent.click(algorithmField);

for (const algorithm of ['Round Robin', 'Least Connections', 'Ring Hash']) {
expect(getByText(algorithm)).toBeVisible();
}
});

it('shows correct session stickiness options for the UDP protocol', async () => {
const { getByLabelText, getByText } = renderWithTheme(
<NodeBalancerConfigPanel
{...nbConfigPanelMockPropsForTest}
protocol="udp"
/>
);

const sessionStickinessField = getByLabelText('Session Stickiness');

expect(sessionStickinessField).toBeVisible();

await userEvent.click(sessionStickinessField);

for (const algorithm of ['None', 'Session', 'Source IP']) {
expect(getByText(algorithm)).toBeVisible();
}
});
});
Loading
Loading