Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
693eaf6
[LKEAPIFW-428] LKE clusters now have IP ACLs
talmai-akamai Sep 19, 2024
7817a17
[LKEAPIFW-428] Migration of non-ipacl'd clusters now working
talmai-akamai Sep 22, 2024
13b644f
[LKEAPIFW-428] Ongoing UI tweaks
talmai-akamai Sep 24, 2024
9ead96e
[LKEAPIFW-428] Additional UI tweaks
talmai-akamai Sep 26, 2024
1eec5c0
[LKEAPIFW-428] Substituition of UI components
talmai-akamai Sep 26, 2024
8c5534f
[LKEAPIFW-428] Another round of UI tweaks: multiline IP default value…
talmai-akamai Sep 30, 2024
d41ff96
fix multiple ip css issues
hana-akamai Oct 1, 2024
265a3ca
[LKEAPIFW-428] Copy text adjustment
talmai-akamai Oct 2, 2024
bc7af8a
add changeset
coliu-akamai Oct 2, 2024
903ce45
Added changeset: ACL related endpoints and types for LKE clusters
coliu-akamai Oct 3, 2024
2bb302b
fix small eslint warnings + update spacing/design review with Daniel
coliu-akamai Oct 4, 2024
27bbbbb
get rid of extra divider, will need to make a few more design updates
coliu-akamai Oct 4, 2024
4af433e
updates as per Daniel's feedback
coliu-akamai Oct 7, 2024
1235902
margin fixes
coliu-akamai Oct 7, 2024
39610eb
height issues
coliu-akamai Oct 7, 2024
672ad55
Merge branch 'develop' into feature/lkeapifw-428
coliu-akamai Oct 8, 2024
900e18f
quick initial cleanup
coliu-akamai Oct 8, 2024
b324a6b
refactor some styling, initial transition to react-hook-form?
coliu-akamai Oct 9, 2024
fcecc60
adding some code description around the update cluster calls
talmai-akamai Oct 10, 2024
34da662
move button file to Kube summary panel
coliu-akamai Oct 10, 2024
1a70f0b
invalidate query after installing ipacl
coliu-akamai Oct 10, 2024
ecbc30c
additional cleanup
coliu-akamai Oct 10, 2024
a5a6b4a
new UX changes, remove refresh
coliu-akamai Oct 11, 2024
6183ec0
make function async
coliu-akamai Oct 11, 2024
dbe9387
some bit more cleanup
coliu-akamai Oct 11, 2024
a67a414
use shape of payload?? need to cleanup
coliu-akamai Oct 15, 2024
10257fb
make more generic
coliu-akamai Oct 15, 2024
d9082e5
cleanup
coliu-akamai Oct 15, 2024
6bc1260
fix validation schema
coliu-akamai Oct 15, 2024
788b1e0
Merge branch 'develop' into feature/lkeapifw-428
coliu-akamai Oct 16, 2024
c0af136
allow empty addresses
coliu-akamai Oct 16, 2024
af7080d
update copies to most recent, add enabled to query
coliu-akamai Oct 17, 2024
0188f60
add footer, address feedback, tag bug
coliu-akamai Oct 17, 2024
1582ac5
address feedback + quick design feedback
coliu-akamai Oct 17, 2024
c871570
design feedback quick change
coliu-akamai Oct 17, 2024
b9e12c7
notice style update
coliu-akamai Oct 17, 2024
2504e61
Merge branch 'develop' into feature/lkeapifw-428
coliu-akamai Oct 21, 2024
b0d9a8a
address feedback
coliu-akamai Oct 21, 2024
169427a
update copies
coliu-akamai Oct 22, 2024
23b2797
fix merge conflict
coliu-akamai Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-10968-added-1727966811522.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Added
---

ACL related endpoints and types for LKE clusters ([#10968](https://github.com/linode/manager/pull/10968))
1 change: 1 addition & 0 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export type AccountCapability =
| 'Kubernetes'
| 'Linodes'
| 'LKE HA Control Planes'
| 'LKE Network Access Control List (IP ACL)'
| 'Machine Images'
| 'Managed Databases'
| 'Managed Databases Beta'
Expand Down
35 changes: 35 additions & 0 deletions packages/api-v4/src/kubernetes/kubernetes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
KubernetesEndpointResponse,
KubernetesDashboardResponse,
KubernetesVersion,
KubernetesControlPlaneACLPayload,
} from './types';

/**
Expand Down Expand Up @@ -221,3 +222,37 @@ export const getKubernetesTypes = (params?: Params) =>
setMethod('GET'),
setParams(params)
);

/**
* getKubernetesClusterControlPlaneACL
*
* Return control plane access list about a single Kubernetes cluster
*/
export const getKubernetesClusterControlPlaneACL = (clusterId: number) =>
Request<KubernetesControlPlaneACLPayload>(
Comment thread
coliu-akamai marked this conversation as resolved.
setMethod('GET'),
setURL(
`${API_ROOT}/lke/clusters/${encodeURIComponent(
clusterId
)}/control_plane_acl`
)
);

/**
* updateKubernetesClusterControlPlaneACL
*
* Update an existing ACL from a single Kubernetes cluster.
*/
export const updateKubernetesClusterControlPlaneACL = (
clusterID: number,
data: Partial<KubernetesControlPlaneACLPayload>
) =>
Request<KubernetesControlPlaneACLPayload>(
setMethod('PUT'),
setURL(
`${API_ROOT}/lke/clusters/${encodeURIComponent(
clusterID
)}/control_plane_acl`
),
setData(data)
);
14 changes: 14 additions & 0 deletions packages/api-v4/src/kubernetes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,22 @@ export interface KubernetesDashboardResponse {
url: string;
}

export interface KubernetesControlPlaneACLPayload {
acl: ControlPlaneACLOptions;
}

export interface ControlPlaneACLOptions {
enabled?: boolean;
'revision-id'?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems quite unusual for the API to introduce a hyphened id property. It doesn't usually play very well with downstream consumers...

addresses?: null | {
ipv4?: null | string[];
ipv6?: null | string[];
};
}

export interface ControlPlaneOptions {
high_availability?: boolean;
acl?: ControlPlaneACLOptions;
}

export interface CreateKubeClusterPayload {
Expand Down
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10968-added-1727901904107.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

IP ACL integration to LKE clusters ([#10968](https://github.com/linode/manager/pull/10968))
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ import { Interception } from 'cypress/types/net-stubbing';
const expectedGranularityArray = ['Auto', '1 day', '1 hr', '5 min'];
const timeDurationToSelect = 'Last 24 Hours';

const { metrics, id, serviceType, dashboardName, region, resource } =
widgetDetails.linode;
const {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this file and factories/dashboards.ts snuck in as eslint fixes while I was resolving conflicts - gonna leave them in this PR

metrics,
id,
serviceType,
dashboardName,
region,
resource,
} = widgetDetails.linode;

const dashboard = dashboardFactory.build({
label: dashboardName,
Expand Down
44 changes: 24 additions & 20 deletions packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Close from '@mui/icons-material/Close';
import { InputBaseProps } from '@mui/material/InputBase';
import Grid from '@mui/material/Unstable_Grid2';
import { Theme } from '@mui/material/styles';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';

Expand All @@ -13,7 +11,10 @@ import { StyledLinkButtonBox } from 'src/components/SelectFirewallPanel/SelectFi
import { TextField } from 'src/components/TextField';
import { TooltipIcon } from 'src/components/TooltipIcon';
import { Typography } from 'src/components/Typography';
import { ExtendedIP } from 'src/utilities/ipUtils';

import type { InputBaseProps } from '@mui/material/InputBase';
import type { Theme } from '@mui/material/styles';
import type { ExtendedIP } from 'src/utilities/ipUtils';

const useStyles = makeStyles()((theme: Theme) => ({
addIP: {
Expand Down Expand Up @@ -57,7 +58,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
},
}));

interface Props {
export interface MultipeIPInputProps {
buttonText?: string;
className?: string;
disabled?: boolean;
Expand All @@ -67,6 +68,7 @@ interface Props {
helperText?: string;
inputProps?: InputBaseProps;
ips: ExtendedIP[];
isLinkStyled?: boolean;
onBlur?: (ips: ExtendedIP[]) => void;
onChange: (ips: ExtendedIP[]) => void;
placeholder?: string;
Expand All @@ -75,7 +77,7 @@ interface Props {
tooltip?: string;
}

export const MultipleIPInput = React.memo((props: Props) => {
export const MultipleIPInput = React.memo((props: MultipeIPInputProps) => {
const {
buttonText,
className,
Expand All @@ -85,6 +87,7 @@ export const MultipleIPInput = React.memo((props: Props) => {
forVPCIPv4Ranges,
helperText,
ips,
isLinkStyled,
onBlur,
onChange,
placeholder,
Expand Down Expand Up @@ -130,21 +133,22 @@ export const MultipleIPInput = React.memo((props: Props) => {
return null;
}

const addIPButton = forVPCIPv4Ranges ? (
<StyledLinkButtonBox>
<LinkButton onClick={addNewInput}>{buttonText}</LinkButton>
</StyledLinkButtonBox>
) : (
<Button
buttonType="secondary"
className={classes.addIP}
compactX
disabled={disabled}
onClick={addNewInput}
>
{buttonText ?? 'Add an IP'}
</Button>
);
const addIPButton =
forVPCIPv4Ranges || isLinkStyled ? (
<StyledLinkButtonBox sx={{ marginTop: isLinkStyled ? '8px' : '12px' }}>
<LinkButton onClick={addNewInput}>{buttonText}</LinkButton>
</StyledLinkButtonBox>
Comment on lines +137 to +140

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

introduced a new prop, isLinkStyled, because of the margin differences (and forVPCIPv4Ranges does additional things) - this does feel a bit jank/not clean, but not too sure what else to do πŸ₯²...will keep thinking but putting this up for review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just pass SX ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm - having an sx prop but not isLinkStyled would get us the secondary Button (lines 140-148) which UX wasn't the biggest fan of (agreed), unless we passed in link styles as sx I think? but then at that point might as well go to the first case

πŸ€”ing more

) : (
<Button
buttonType="secondary"
className={classes.addIP}
compactX
disabled={disabled}
onClick={addNewInput}
>
{buttonText ?? 'Add an IP'}
</Button>
);

return (
<div className={cx(classes.root, className)}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';

import { MultipleIPInput } from './MultipleIPInput';

import type { MultipeIPInputProps } from './MultipleIPInput';
import type { FieldError, Merge } from 'react-hook-form';
import type { ExtendedIP } from 'src/utilities/ipUtils';

interface Props extends Omit<MultipeIPInputProps, 'ips' | 'onChange'> {
ipErrors?: Merge<FieldError, (FieldError | undefined)[]>;
nonExtendedIPs: string[];
onNonExtendedIPChange: (ips: string[]) => void;
}

/**
* Quick wrapper for MultipleIPInput so that we do not have to directly use the type ExtendedIP (which has its own error field)
*
* I wanted to avoid touching MultipleIPInput too much, since a lot of other flows use that component. This component was
* made with 'react-hook-form' in mind, taking in 'react-hook-form' errors and mapping them to the given (non
* extended) IPs. We might eventually try to completely remove the ExtendedIP type - see
* https://github.com/linode/manager/pull/10968#discussion_r1800089369 for context
*/
export const MultipleNonExtendedIPInput = (props: Props) => {
const { ipErrors, nonExtendedIPs, onNonExtendedIPChange, ...rest } = props;

const extendedIPs: ExtendedIP[] =
nonExtendedIPs.map((ip, idx) => {
return {
address: ip,
error: ipErrors ? ipErrors[idx]?.message : '',
};
}) ?? [];

return (
<MultipleIPInput
{...rest}
onChange={(ips) => {
const _ips = ips.map((ip) => {
return ip.address;
});
onNonExtendedIPChange(_ips);
}}
ips={extendedIPs}
/>
);
};
21 changes: 12 additions & 9 deletions packages/manager/src/factories/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export const widgetFactory = Factory.Sync.makeFactory<Widgets>({
y_label: Factory.each((i) => `y_label_${i}`),
});

export const dashboardMetricFactory =
Factory.Sync.makeFactory<AvailableMetrics>({
export const dashboardMetricFactory = Factory.Sync.makeFactory<AvailableMetrics>(
{
available_aggregate_functions: ['min', 'max', 'avg', 'sum'],
dimensions: [],
label: Factory.each((i) => `widget_label_${i}`),
Expand All @@ -62,25 +62,28 @@ export const dashboardMetricFactory =
(i) => scrape_interval[i % scrape_interval.length]
),
unit: 'defaultUnit',
});
}
);

export const cloudPulseMetricsResponseDataFactory =
Factory.Sync.makeFactory<CloudPulseMetricsResponseData>({
export const cloudPulseMetricsResponseDataFactory = Factory.Sync.makeFactory<CloudPulseMetricsResponseData>(
{
result: [
{
metric: {},
values: [],
},
],
result_type: 'matrix',
});
}
);

export const cloudPulseMetricsResponseFactory =
Factory.Sync.makeFactory<CloudPulseMetricsResponse>({
export const cloudPulseMetricsResponseFactory = Factory.Sync.makeFactory<CloudPulseMetricsResponse>(
{
data: cloudPulseMetricsResponseDataFactory.build(),
isPartial: false,
stats: {
series_fetched: 2,
},
status: 'success',
});
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { FormLabel } from '@mui/material';
import * as React from 'react';

import { Box } from 'src/components/Box';
import { ErrorMessage } from 'src/components/ErrorMessage';
import { FormControl } from 'src/components/FormControl';
import { FormControlLabel } from 'src/components/FormControlLabel';
import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput';
import { Notice } from 'src/components/Notice/Notice';
import { Toggle } from 'src/components/Toggle/Toggle';
import { Typography } from 'src/components/Typography';
import { validateIPs } from 'src/utilities/ipUtils';

import type { ExtendedIP } from 'src/utilities/ipUtils';

export interface ControlPlaneACLProps {
enableControlPlaneACL: boolean;
errorText: string | undefined;
handleIPv4Change: (ips: ExtendedIP[]) => void;
handleIPv6Change: (ips: ExtendedIP[]) => void;
ipV4Addr: ExtendedIP[];
ipV6Addr: ExtendedIP[];
setControlPlaneACL: (enabled: boolean) => void;
}

export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => {
const {
enableControlPlaneACL,
errorText,
handleIPv4Change,
handleIPv6Change,
ipV4Addr,
ipV6Addr,
setControlPlaneACL,
} = props;

return (
<>
<FormControl data-testid="control-plane-ipacl-form">
<FormLabel id="ipacl-radio-buttons-group-label">
<Typography variant="inherit">Control Plane ACL</Typography>
</FormLabel>
{errorText && (
<Notice spacingTop={8} variant="error">
<ErrorMessage message={errorText} />{' '}
</Notice>
)}
<Typography mb={1} sx={{ width: '85%' }}>
Enable an access control list (ACL) on your LKE cluster to restrict
access to your cluster’s control plane. When enabled, only the IP
addresses and ranges specified by you can connect to the control
plane.
</Typography>
<FormControlLabel
control={
<Toggle
checked={enableControlPlaneACL}
name="ipacl-checkbox"
onChange={() => setControlPlaneACL(!enableControlPlaneACL)}
/>
}
label="Enable Control Plane ACL"
/>
</FormControl>
{enableControlPlaneACL && (
<Box sx={{ marginBottom: 3, maxWidth: 450 }}>
<MultipleIPInput
onBlur={(_ips: ExtendedIP[]) => {
const validatedIPs = validateIPs(_ips, {
allowEmptyAddress: true,
errorMessage: 'Must be a valid IPv4 address.',
});
handleIPv4Change(validatedIPs);
}}
buttonText="Add IPv4 Address"
ips={ipV4Addr}
isLinkStyled
onChange={handleIPv4Change}
placeholder="0.0.0.0/0"
title="IPv4 Addresses or CIDRs"
/>
<Box marginTop={2}>
<MultipleIPInput
onBlur={(_ips: ExtendedIP[]) => {
const validatedIPs = validateIPs(_ips, {
allowEmptyAddress: true,
errorMessage: 'Must be a valid IPv6 address.',
});
handleIPv6Change(validatedIPs);
}}
buttonText="Add IPv6 Address"
ips={ipV6Addr}
isLinkStyled
onChange={handleIPv6Change}
placeholder="::/0"
title="IPv6 Addresses or CIDRs"
/>
</Box>
</Box>
)}
</>
);
};
Loading