Skip to content

Commit

Permalink
feat: [LKEAPIFW-428] LKE clusters should have IP ACL integration on C…
Browse files Browse the repository at this point in the history
…M (part 1) (#10968)

* [LKEAPIFW-428] LKE clusters now have IP ACLs

* [LKEAPIFW-428] Migration of non-ipacl'd clusters now working

* [LKEAPIFW-428] Ongoing UI tweaks

* [LKEAPIFW-428] Additional UI tweaks

* [LKEAPIFW-428] Substituition of UI components

* [LKEAPIFW-428] Another round of UI tweaks: multiline IP default values; IPACL drawer showing enabled only when enabled

wip

* fix multiple ip css issues

* [LKEAPIFW-428] Copy text adjustment

* add changeset

* Added changeset: ACL related endpoints and types for LKE clusters

* fix small eslint warnings + update spacing/design review with Daniel

* get rid of extra divider, will need to make a few more design updates

* updates as per Daniel's feedback

* margin fixes

* height issues

* quick initial cleanup

* refactor some styling, initial transition to react-hook-form?

* adding some code description around the update cluster calls

* move button file to Kube summary panel

* invalidate query after installing ipacl

* additional cleanup

* new UX changes, remove refresh

* make function async

* some bit more cleanup

* use shape of payload?? need to cleanup

* make more generic

* cleanup

* fix validation schema

* allow empty addresses

* update copies to most recent, add enabled to query

* add footer, address feedback, tag bug

* address feedback + quick design feedback

* design feedback quick change

* notice style update

* address feedback

* update copies

---------

Co-authored-by: Talmai Oliveira <toliveir@akamai.com>
Co-authored-by: Hana Xu <hxu@akamai.com>
Co-authored-by: Connie Liu <coliu@akamai.com>
  • Loading branch information
4 people authored Oct 22, 2024
1 parent b64a39d commit 8b9d956
Show file tree
Hide file tree
Showing 22 changed files with 1,034 additions and 186 deletions.
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>(
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;
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 {
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>
) : (
<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

0 comments on commit 8b9d956

Please sign in to comment.