From 693eaf6a1c6ea84ca0eec81ed7228ff15553fe70 Mon Sep 17 00:00:00 2001 From: Talmai Oliveira Date: Thu, 19 Sep 2024 01:43:57 -0400 Subject: [PATCH 01/36] [LKEAPIFW-428] LKE clusters now have IP ACLs --- packages/api-v4/src/account/types.ts | 1 + packages/api-v4/src/kubernetes/kubernetes.ts | 36 +++ packages/api-v4/src/kubernetes/types.ts | 14 + .../CreateCluster/ControlPlaneACLPane.tsx | 114 ++++++++ .../CreateCluster/CreateCluster.tsx | 45 ++- .../KubeClusterControlPlaneACL.tsx | 164 +++++++++++ .../KubeControlPaneACLDrawer.tsx | 276 ++++++++++++++++++ .../KubeSummaryPanel.tsx | 40 +++ .../src/features/Kubernetes/kubeUtils.ts | 17 ++ packages/manager/src/queries/kubernetes.ts | 33 +++ 10 files changed, 739 insertions(+), 1 deletion(-) create mode 100644 packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx create mode 100644 packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx create mode 100644 packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 39211183930..918c761d631 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -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' diff --git a/packages/api-v4/src/kubernetes/kubernetes.ts b/packages/api-v4/src/kubernetes/kubernetes.ts index b80c011d4cf..97886c27b43 100644 --- a/packages/api-v4/src/kubernetes/kubernetes.ts +++ b/packages/api-v4/src/kubernetes/kubernetes.ts @@ -15,6 +15,7 @@ import type { KubernetesEndpointResponse, KubernetesDashboardResponse, KubernetesVersion, + KubernetesControlPlaneACLPayload, } from './types'; /** @@ -192,3 +193,38 @@ 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( + 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 +) => + Request( + setMethod('PUT'), + setURL( + `${API_ROOT}/lke/clusters/${encodeURIComponent( + clusterID + )}/control_plane_acl` + ), + setData(data) + ); diff --git a/packages/api-v4/src/kubernetes/types.ts b/packages/api-v4/src/kubernetes/types.ts index 8e2d176572c..3d4cdfc387d 100644 --- a/packages/api-v4/src/kubernetes/types.ts +++ b/packages/api-v4/src/kubernetes/types.ts @@ -59,8 +59,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 { diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx new file mode 100644 index 00000000000..b678b01d39c --- /dev/null +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -0,0 +1,114 @@ +import { FormLabel } from '@mui/material'; +import * as React from 'react'; + +import { FormControl } from 'src/components/FormControl'; +import { Link } from 'src/components/Link'; +import { Typography } from 'src/components/Typography'; +import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; +import { ExtendedIP, validateIPs } from 'src/utilities/ipUtils'; +import { Notice } from 'src/components/Notice/Notice'; +import { Checkbox } from 'src/components/Checkbox'; +import Stack from '@mui/material/Stack'; + +export interface ControlPlaneACLProps { + enableControlPlaneACL: boolean; + setControlPlaneACL: (enabled: boolean) => void; + ipV4Addr: ExtendedIP[]; + handleIPv4Change: (ips: ExtendedIP[]) => void; + ipV6Addr: ExtendedIP[]; + handleIPv6Change: (ips: ExtendedIP[]) => void; + aclError?: string; +} + +export const IPACLCopy = () => ( + + This is the text for Control Plane Access Control. +
+ + Learn more about the control plane access control list + + . +
+); + +export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { + const { + enableControlPlaneACL, + setControlPlaneACL, + ipV4Addr, + handleIPv4Change, + ipV6Addr, + handleIPv6Change, + aclError, + } = props; + + const handleChange = (e: React.ChangeEvent) => { + setControlPlaneACL(!enableControlPlaneACL); + }; + + const statusStyle = (status: boolean) => { + switch (status) { + case false: + return 'none'; + default: + return ''; + } + }; + + const [inputError, setInputError] = React.useState(''); + + return ( + + {aclError && } + ({ + '&&.MuiFormLabel-root.Mui-focused': { + color: theme.name === 'dark' ? 'white' : theme.color.black, + }, + })} + id="ipacl-radio-buttons-group-label" + > + + Control Plane Access Control (IPACL) + + + + handleChange(e)} + /> + + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }); + const ipsWithErrors = validatedIPs.filter((thisIP) => + setInputError(thisIP.error) + ); + if (ipsWithErrors.length === 0) { + handleIPv4Change(validatedIPs); + } + }} + placeholder="0.0.0.0/0" + title="" // Empty string so a title isn't displayed for each IP input + error={inputError} + /> + { + handleIPv6Change(newIpV6Addr); + }} + placeholder="::/0" + title="" // Empty string so a title isn't displayed for each IP input + /> + + + ); +}; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index d5122dc8736..8c35a735cc2 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -18,6 +18,7 @@ import { RegionHelperText } from 'src/components/SelectRegionPanel/RegionHelperT import { Stack } from 'src/components/Stack'; import { TextField } from 'src/components/TextField'; import { + getKubeControlPlaneACL, getKubeHighAvailability, getLatestVersion, } from 'src/features/Kubernetes/kubeUtils'; @@ -49,7 +50,9 @@ import { useStyles, } from './CreateCluster.styles'; import { HAControlPlane } from './HAControlPlane'; +import { ControlPlaneACLPane } from './ControlPlaneACLPane'; import { NodePoolPanel } from './NodePoolPanel'; +import { ExtendedIP, stringToExtendedIP } from 'src/utilities/ipUtils'; import type { CreateKubeClusterPayload, @@ -72,12 +75,20 @@ export const CreateCluster = () => { const formContainerRef = React.useRef(null); const { mutateAsync: updateAccountAgreements } = useMutateAccountAgreements(); const [highAvailability, setHighAvailability] = React.useState(); + const [controlPlaneACL, setControlPlaneACL] = React.useState(true); const { data, error: regionsError } = useRegionsQuery(); const regionsData = data ?? []; const history = useHistory(); const { data: account } = useAccount(); const { showHighAvailability } = getKubeHighAvailability(account); + const { showControlPlaneACL } = getKubeControlPlaneACL(account); + const [ipV4Addr, setIPv4Addr] = React.useState([ + stringToExtendedIP('0.0.0.0/0'), + ]); + const [ipV6Addr, setIPv6Addr] = React.useState([ + stringToExtendedIP('::/0'), + ]); const { data: kubernetesHighAvailabilityTypesData, @@ -128,8 +139,23 @@ export const CreateCluster = () => { pick(['type', 'count']) ) as CreateNodePoolData[]; + const _ipv4 = ipV4Addr.map((ip) => { + return ip.address; + }); + + const _ipv6 = ipV6Addr.map((ip) => { + return ip.address; + }); + const payload: CreateKubeClusterPayload = { - control_plane: { high_availability: highAvailability ?? false }, + control_plane: { + high_availability: highAvailability ?? false, + acl: { + enabled: controlPlaneACL, + 'revision-id': '', + addresses: { ipv4: _ipv4, ipv6: _ipv6 }, + }, + }, k8s_version: version, label, node_pools, @@ -281,6 +307,23 @@ export const CreateCluster = () => { /> ) : null} + + {showControlPlaneACL ? ( + + { + setIPv4Addr(newIpV4Addr); + }} + ipV6Addr={ipV6Addr} + handleIPv6Change={(newIpV6Addr: ExtendedIP[]) => { + setIPv6Addr(newIpV6Addr); + }} + /> + + ) : null} void; +} + +const useStyles = makeStyles()((theme: Theme) => ({ + iconTextOuter: { + flexBasis: '72%', + minWidth: 115, + }, + item: { + '&:first-of-type': { + paddingTop: 0, + }, + '&:last-of-type': { + paddingBottom: 0, + }, + paddingBottom: theme.spacing(1), + paddingTop: theme.spacing(1), + }, + mainGridContainer: { + position: 'relative', + [theme.breakpoints.up('lg')]: { + justifyContent: 'space-between', + }, + }, + root: { + marginBottom: theme.spacing(3), + padding: `${theme.spacing(2.5)} ${theme.spacing(2.5)} ${theme.spacing(3)}`, + }, + tooltip: { + '& .MuiTooltip-tooltip': { + minWidth: 320, + }, + }, + aclElement: { + '&:hover': { + opacity: 0.7, + }, + '&:last-child': { + borderRight: 'none', + }, + alignItems: 'center', + borderRight: '1px solid #c4c4c4', + cursor: 'pointer', + display: 'flex', + }, + aclElementText: { + color: theme.textColors.linkActiveLight, + marginRight: theme.spacing(1), + whiteSpace: 'nowrap', + }, +})); + +export const KubeClusterControlPlaneACL = React.memo((props: Props) => { + const theme = useTheme(); + const { cluster, handleOpenDrawer } = props; + const { classes } = useStyles(); + + const { + data: acl_response, + isError: isErrorKubernetesACL, + isLoading: isLoadingKubernetesACL, + } = useKubernetesControlPlaneACLQuery(cluster.id); + + const enabledACL = acl_response ? acl_response.acl.enabled : false; + //const revisionIDACL = acl_response ? acl_response.acl['revision-id'] : ''; + const totalIPv4 = acl_response?.acl.addresses?.ipv4?.length + ? acl_response?.acl.addresses?.ipv4?.length + : 0; + const totalIPv6 = acl_response?.acl.addresses?.ipv6?.length + ? acl_response?.acl.addresses?.ipv6?.length + : 0; + const totalNumberIPs = totalIPv4 + totalIPv6; + + const IPACLdClusterToolTip = () => { + return ( + + ); + }; + + const EnabledCopy = () => { + return ( + <> + + + {pluralize('IP Address', 'IP Addresses', totalNumberIPs)} + + + + ); + }; + + const DisabledCopy = () => { + return ( + <> + + + IPACL Disabled + + + + ); + }; + + const NotMigratedCopy = () => { + return ( + <> + Cluster Requires Migration + + + ); + }; + + const kubeSpecsLeft = [ + isLoadingKubernetesACL ? ( + + ) : isErrorKubernetesACL ? ( + + ) : enabledACL ? ( + + ) : ( + + ), + ]; + + return ( + + {kubeSpecsLeft} + + ); +}); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx new file mode 100644 index 00000000000..7c03d0c1718 --- /dev/null +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -0,0 +1,276 @@ +import Grid from '@mui/material/Unstable_Grid2'; +import * as React from 'react'; + +import { Drawer } from 'src/components/Drawer'; +import { DrawerContent } from 'src/components/DrawerContent'; +import { Typography } from 'src/components/Typography'; +import { + useKubernetesControlPlaneACLMutation, + useKubernetesControlPlaneACLQuery, +} from 'src/queries/kubernetes'; +import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; +import { + ExtendedIP, + validateIPs, + stringToExtendedIP, +} from 'src/utilities/ipUtils'; +import { Checkbox } from 'src/components/Checkbox'; +import { TextField } from 'src/components/TextField'; +import { Stack, Divider } from '@mui/material'; +import { StyledButton } from 'src/components/CheckoutBar/styles'; +import { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; +import { Notice } from 'src/components/Notice/Notice'; + +interface Props { + closeDrawer: () => void; + clusterId: number; + clusterLabel: string; + open: boolean; +} + +export const KubeControlPlaneACLDrawer = (props: Props) => { + const { closeDrawer, clusterId, clusterLabel, open } = props; + + const [ipV4InputError, setIPV4InputError] = React.useState< + string | undefined + >(''); + const [updateError, setUpdateACLError] = React.useState(); + const [updating, setUpdating] = React.useState(false); + + const { + data: data, + error: isErrorKubernetesACL, + isLoading: isLoadingKubernetesACL, + isFetching: isFetchingKubernetesACL, + refetch: refetchKubernetesACL, + } = useKubernetesControlPlaneACLQuery(clusterId); + + // dynamic variables mapped to JSON queried for this cluster + const _ipv4 = data?.acl?.addresses?.ipv4?.map((ip) => { + return stringToExtendedIP(ip); + }); + + const _ipv6 = data?.acl?.addresses?.ipv6?.map((ip) => { + return stringToExtendedIP(ip); + }); + + const _enabled = data?.acl?.enabled; + + const _revisionID = data?.acl?.['revision-id']; + + // respective react states + const [ipV4Addr, setIPv4Addr] = React.useState([]); + const [ipV6Addr, setIPv6Addr] = React.useState([]); + const [controlPlaneACL, setControlPlaneACL] = React.useState(false); + const [revisionID, setRevisionID] = React.useState(); + + // refetchOnMount isnt good enough for this query because + // it is already mounted in the rendered Drawer + React.useEffect(() => { + if (open && !isLoadingKubernetesACL && !isFetchingKubernetesACL) { + // updates states based on queried data + setIPv4Addr(_ipv4 ? _ipv4 : []); + setIPv6Addr(_ipv6 ? _ipv6 : []); + setControlPlaneACL(_enabled ? _enabled : false); + setRevisionID(_revisionID ? _revisionID : ''); + setUpdateACLError(isErrorKubernetesACL?.[0].reason); + setUpdating(false); + refetchKubernetesACL(); + } + }, [open]); + + const { + mutateAsync: updateKubernetesClusterControlPlaneACL, + } = useKubernetesControlPlaneACLMutation(clusterId); + + const updateCluster = () => { + setUpdateACLError(undefined); + setUpdating(true); + + const _newIPv4 = ipV4Addr.map((ip) => { + return ip.address; + }); + + const _newIPv6 = ipV6Addr.map((ip) => { + return ip.address; + }); + + const payload: KubernetesControlPlaneACLPayload = { + acl: { + enabled: controlPlaneACL, + 'revision-id': revisionID, + addresses: { ipv4: _newIPv4, ipv6: _newIPv6 }, + }, + }; + + updateKubernetesClusterControlPlaneACL(payload) + .then(() => { + closeDrawer(); + setUpdating(false); + }) + .catch((err) => { + const regex = /(?<=\bControl\b: ).*/; + setUpdateACLError(err[0].reason.match(regex)); + }); + + setUpdating(false); + }; + + const ErrorMessage = () => { + if (!!updateError) { + return ( + + {updateError} + + ); + } + return <>; + }; + + const EnabledCopy = () => { + return ( + <> + + + Enabled + + + + A value of true results in a default policy of DENY. A value of + false results in a default policy of ALLOW (i.e., access controls + are disabled). When enabled, control plane access controls can + only be accessible through the defined IP CIDRs. + + + + + ); + }; + + const RevisionIDCopy = () => { + return ( + <> + + + Revision ID + + + + Enables clients to track events related to ACL update requests and + enforcements. Optional field. If omitted, defaults to a randomly + generated string. + + + + + ); + }; + + const AddressesCopy = () => { + return ( + <> + + Addresses + + + + A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW + access to the control plane. + + + + ); + }; + + return ( + + + + + + {clusterLabel} + + + + When a cluster is equipped with an ACL, the apiserver and + dashboard endpoints get mapped to a NodeBalancer address where + all traffic is protected through a Cloud Firewall. + + + + + + + setControlPlaneACL(e.target.checked)} + /> + + + + setRevisionID(e.target.value)} + /> + + + + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }); + const ipsWithErrors: ExtendedIP[] = validatedIPs.filter( + (thisIP) => setIPV4InputError(thisIP.error) + ); + if (ipsWithErrors.length === 0) { + setIPv4Addr(validatedIPs); + } + }} + placeholder="0.0.0.0/0" + title="" // Empty string so a title isn't displayed for each IP input + error={ipV4InputError} + /> + { + setIPv6Addr(newIpV6Addr); + }} + placeholder="::/0" + title="" // Empty string so a title isn't displayed for each IP input + /> + + + + Update + + + + + + ); +}; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index a3579c98c27..864a9887b9a 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -16,6 +16,7 @@ import { Stack } from 'src/components/Stack'; import { TagCell } from 'src/components/TagCell/TagCell'; import { Typography } from 'src/components/Typography'; import { KubeClusterSpecs } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs'; +import { KubeClusterControlPlaneACL } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useKubernetesClusterMutation, @@ -27,6 +28,7 @@ import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { DeleteKubernetesClusterDialog } from './DeleteKubernetesClusterDialog'; import { KubeConfigDisplay } from './KubeConfigDisplay'; import { KubeConfigDrawer } from './KubeConfigDrawer'; +import { KubeControlPlaneACLDrawer } from './KubeControlPaneACLDrawer'; import type { KubernetesCluster } from '@linode/api-v4/lib/kubernetes'; import type { Theme } from '@mui/material/styles'; @@ -107,6 +109,10 @@ export const KubeSummaryPanel = React.memo((props: Props) => { const { enqueueSnackbar } = useSnackbar(); const [drawerOpen, setDrawerOpen] = React.useState(false); + const [ + isControlPlaneACLDrawerOpen, + setControlPlaneACLDrawerOpen, + ] = React.useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( @@ -244,6 +250,34 @@ export const KubeSummaryPanel = React.memo((props: Props) => { } + footer={ + <> + + IPACL + + + setControlPlaneACLDrawerOpen(true)} + /> + + + } noBodyBottomBorder /> @@ -253,6 +287,12 @@ export const KubeSummaryPanel = React.memo((props: Props) => { clusterLabel={cluster.label} open={drawerOpen} /> + setControlPlaneACLDrawerOpen(false)} + clusterId={cluster.id} + clusterLabel={cluster.label} + open={isControlPlaneACLDrawerOpen} + /> { + const showControlPlaneACL = account?.capabilities.includes( + 'LKE Network Access Control List (IP ACL)' + ); + + const isClusterControlPlaneACLd = Boolean( + showControlPlaneACL && cluster?.control_plane.acl + ); + + return { + isClusterControlPlaneACLd, + showControlPlaneACL, + }; +}; /** * Retrieves the latest version from an array of version objects. * diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index 86b79eaf662..7108e5a456d 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -10,6 +10,7 @@ import { getKubernetesClusters, getKubernetesTypes, getKubernetesVersions, + getKubernetesClusterControlPlaneACL, getNodePools, recycleAllNodes, recycleClusterNodes, @@ -17,6 +18,7 @@ import { resetKubeConfig, updateKubernetesCluster, updateNodePool, + updateKubernetesClusterControlPlaneACL, } from '@linode/api-v4'; import { createQueryKeys } from '@lukemorales/query-key-factory'; import { @@ -36,6 +38,7 @@ import type { CreateNodePoolData, KubeNodePoolResponse, KubernetesCluster, + KubernetesControlPlaneACLPayload, KubernetesDashboardResponse, KubernetesEndpointResponse, KubernetesVersion, @@ -71,6 +74,10 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { queryFn: () => getAllNodePoolsForCluster(id), queryKey: null, }, + acl: { + queryFn: () => getKubernetesClusterControlPlaneACL(id), + queryKey: null, + }, }, queryFn: () => getKubernetesCluster(id), queryKey: [id], @@ -312,6 +319,32 @@ export const useAllKubernetesClustersQuery = (enabled = false) => { }); }; +export const useKubernetesControlPlaneACLQuery = (clusterId: number) => { + return useQuery( + kubernetesQueries.cluster(clusterId)._ctx.acl + ); +}; + +export const useKubernetesControlPlaneACLMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation< + KubernetesControlPlaneACLPayload, + APIError[], + Partial + >({ + mutationFn: (data) => updateKubernetesClusterControlPlaneACL(id, data), + onSuccess(data) { + queryClient.invalidateQueries({ + queryKey: kubernetesQueries.cluster(id)._ctx.acl.queryKey, + }); + queryClient.setQueryData( + kubernetesQueries.cluster(id)._ctx.acl.queryKey, + data + ); + }, + }); +}; + const getAllNodePoolsForCluster = (clusterId: number) => getAll((params, filters) => getNodePools(clusterId, params, filters) From 7817a172b594c958d9cd66ed4c772b197e7b7443 Mon Sep 17 00:00:00 2001 From: Talmai Oliveira Date: Sun, 22 Sep 2024 01:48:04 -0400 Subject: [PATCH 02/36] [LKEAPIFW-428] Migration of non-ipacl'd clusters now working --- .../CreateCluster/ControlPlaneACLPane.tsx | 33 ++-- .../KubeClusterControlPlaneACL.tsx | 19 ++- .../KubeControlPaneACLDrawer.tsx | 143 ++++++++++++++---- .../KubeSummaryPanel.tsx | 6 + 4 files changed, 155 insertions(+), 46 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index b678b01d39c..7315e03b62a 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -55,7 +55,12 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { } }; - const [inputError, setInputError] = React.useState(''); + const [ipV4InputError, setIPV4InputError] = React.useState< + string | undefined + >(''); + const [ipV6InputError, setIPV6InputError] = React.useState< + string | undefined + >(''); return ( @@ -81,7 +86,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { /> { const validatedIPs = validateIPs(_ips, { @@ -89,24 +94,34 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { errorMessage: 'Must be a valid IPv4 address.', }); const ipsWithErrors = validatedIPs.filter((thisIP) => - setInputError(thisIP.error) + setIPV4InputError(thisIP.error) ); if (ipsWithErrors.length === 0) { handleIPv4Change(validatedIPs); } }} placeholder="0.0.0.0/0" - title="" // Empty string so a title isn't displayed for each IP input - error={inputError} + title="IPv4 Addresses or CIDR" + error={ipV4InputError} /> { - handleIPv6Change(newIpV6Addr); + onChange={(_ips: ExtendedIP[]) => { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv6 address.', + }); + const ipsWithErrors: ExtendedIP[] = validatedIPs.filter((thisIP) => + setIPV6InputError(thisIP.error) + ); + if (ipsWithErrors.length === 0) { + handleIPv6Change(validatedIPs); + } }} placeholder="::/0" - title="" // Empty string so a title isn't displayed for each IP input + title="IPv6 Addresses or CIDR" + error={ipV6InputError} /> diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx index 4f7748c5a24..eb5af09015a 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx @@ -15,6 +15,7 @@ import type { Theme } from '@mui/material/styles'; interface Props { cluster: KubernetesCluster; + setControlPlaneACLMigrated: (s: boolean) => void; handleOpenDrawer: () => void; } @@ -69,7 +70,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { const theme = useTheme(); - const { cluster, handleOpenDrawer } = props; + const { cluster, handleOpenDrawer, setControlPlaneACLMigrated } = props; const { classes } = useStyles(); const { @@ -88,6 +89,12 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { : 0; const totalNumberIPs = totalIPv4 + totalIPv6; + const failedMigrationStatus = () => { + // when a cluster has not migrated, the query will always fail + setControlPlaneACLMigrated(!isErrorKubernetesACL); + return isErrorKubernetesACL; + }; + const IPACLdClusterToolTip = () => { return ( { const NotMigratedCopy = () => { return ( <> - Cluster Requires Migration - + + + Cluster Requires Migration + + + ); }; @@ -140,7 +151,7 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { const kubeSpecsLeft = [ isLoadingKubernetesACL ? ( - ) : isErrorKubernetesACL ? ( + ) : failedMigrationStatus() ? ( ) : enabledACL ? ( diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 7c03d0c1718..9e94bf121e9 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -7,6 +7,7 @@ import { Typography } from 'src/components/Typography'; import { useKubernetesControlPlaneACLMutation, useKubernetesControlPlaneACLQuery, + useKubernetesClusterMutation, } from 'src/queries/kubernetes'; import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; import { @@ -25,15 +26,19 @@ interface Props { closeDrawer: () => void; clusterId: number; clusterLabel: string; + clusterMigrated: boolean; open: boolean; } export const KubeControlPlaneACLDrawer = (props: Props) => { - const { closeDrawer, clusterId, clusterLabel, open } = props; + const { closeDrawer, clusterId, clusterLabel, clusterMigrated, open } = props; const [ipV4InputError, setIPV4InputError] = React.useState< string | undefined >(''); + const [ipV6InputError, setIPV6InputError] = React.useState< + string | undefined + >(''); const [updateError, setUpdateACLError] = React.useState(); const [updating, setUpdating] = React.useState(false); @@ -69,11 +74,17 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { React.useEffect(() => { if (open && !isLoadingKubernetesACL && !isFetchingKubernetesACL) { // updates states based on queried data - setIPv4Addr(_ipv4 ? _ipv4 : []); - setIPv6Addr(_ipv6 ? _ipv6 : []); + setIPv4Addr( + _ipv4 ? _ipv4 : clusterMigrated ? [] : [stringToExtendedIP('0.0.0.0/0')] + ); + setIPv6Addr( + _ipv6 ? _ipv6 : clusterMigrated ? [] : [stringToExtendedIP('::/0')] + ); setControlPlaneACL(_enabled ? _enabled : false); setRevisionID(_revisionID ? _revisionID : ''); setUpdateACLError(isErrorKubernetesACL?.[0].reason); + setIPV4InputError(undefined); + setIPV6InputError(undefined); setUpdating(false); refetchKubernetesACL(); } @@ -83,6 +94,10 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { mutateAsync: updateKubernetesClusterControlPlaneACL, } = useKubernetesControlPlaneACLMutation(clusterId); + const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( + clusterId + ); + const updateCluster = () => { setUpdateACLError(undefined); setUpdating(true); @@ -97,27 +112,41 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const payload: KubernetesControlPlaneACLPayload = { acl: { - enabled: controlPlaneACL, + enabled: clusterMigrated ? controlPlaneACL : true, // new cluster installations default to true 'revision-id': revisionID, addresses: { ipv4: _newIPv4, ipv6: _newIPv6 }, }, }; - updateKubernetesClusterControlPlaneACL(payload) - .then(() => { - closeDrawer(); - setUpdating(false); + if (clusterMigrated) { + updateKubernetesClusterControlPlaneACL(payload) + .then(() => { + closeDrawer(); + setUpdating(false); + }) + .catch((err) => { + const regex = /(?<=\bControl\b: ).*/; + setUpdateACLError(err[0].reason.match(regex)); + setUpdating(false); + }); + } else { + updateKubernetesCluster({ + control_plane: payload, }) - .catch((err) => { - const regex = /(?<=\bControl\b: ).*/; - setUpdateACLError(err[0].reason.match(regex)); - }); - - setUpdating(false); + .then((_) => { + closeDrawer(); + setUpdating(false); + }) + .catch((err) => { + const regex = /(?<=\bControl\b: ).*/; + setUpdateACLError(err[0].reason.match(regex)); + setUpdating(false); + }); + } }; const ErrorMessage = () => { - if (!!updateError) { + if (!!updateError && clusterMigrated) { return ( {updateError} @@ -127,6 +156,22 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { return <>; }; + const ClusterNeedsMigration = () => { + if (!clusterMigrated) { + return ( + <> + + IPACL is not yet installed on this cluster.... may take up to 10 + minutes before.. + + + + ); + } else { + return <>; + } + }; + const EnabledCopy = () => { return ( <> @@ -147,6 +192,25 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { ); }; + const RevisionID = () => { + if (clusterMigrated) { + return ( + <> + + setRevisionID(e.target.value)} + /> + + + ); + } else { + return <>; + } + }; + const RevisionIDCopy = () => { return ( <> @@ -182,6 +246,10 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { ); }; + const SubmitButtonCopy = () => { + return clusterMigrated ? 'Update' : 'Install'; + }; + return ( { wide > { + + setControlPlaneACL(e.target.checked)} + onChange={(e) => { + if (clusterMigrated) { + return setControlPlaneACL(e.target.checked); + } + }} /> - - setRevisionID(e.target.value)} - /> - + { const validatedIPs = validateIPs(_ips, { @@ -245,17 +312,27 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { } }} placeholder="0.0.0.0/0" - title="" // Empty string so a title isn't displayed for each IP input + title="IPv4 Addresses or CIDR" error={ipV4InputError} /> { - setIPv6Addr(newIpV6Addr); + onChange={(_ips: ExtendedIP[]) => { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv6 address.', + }); + const ipsWithErrors: ExtendedIP[] = validatedIPs.filter( + (thisIP) => setIPV6InputError(thisIP.error) + ); + if (ipsWithErrors.length === 0) { + setIPv6Addr(validatedIPs); + } }} placeholder="::/0" - title="" // Empty string so a title isn't displayed for each IP input + title="IPv6 Addresses or CIDR" + error={ipV6InputError} /> @@ -266,7 +343,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { onClick={updateCluster} disabled={!!ipV4InputError} > - Update + diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index 864a9887b9a..7c029d208ae 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -113,6 +113,10 @@ export const KubeSummaryPanel = React.memo((props: Props) => { isControlPlaneACLDrawerOpen, setControlPlaneACLDrawerOpen, ] = React.useState(false); + const [ + isControlPlaneACLMigrated, + setControlPlaneACLMigrated, + ] = React.useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( @@ -273,6 +277,7 @@ export const KubeSummaryPanel = React.memo((props: Props) => { > setControlPlaneACLDrawerOpen(true)} /> @@ -291,6 +296,7 @@ export const KubeSummaryPanel = React.memo((props: Props) => { closeDrawer={() => setControlPlaneACLDrawerOpen(false)} clusterId={cluster.id} clusterLabel={cluster.label} + clusterMigrated={isControlPlaneACLMigrated} open={isControlPlaneACLDrawerOpen} /> Date: Tue, 24 Sep 2024 01:47:08 -0400 Subject: [PATCH 03/36] [LKEAPIFW-428] Ongoing UI tweaks --- .../CreateCluster/ControlPlaneACLPane.tsx | 20 +- .../KubeClusterControlPlaneACL.tsx | 26 +-- .../KubeControlPaneACLDrawer.tsx | 31 +-- .../KubeSummaryPanel.styles.tsx | 201 ++++++++++++++++++ .../KubeSummaryPanel.tsx | 75 ++++--- 5 files changed, 279 insertions(+), 74 deletions(-) create mode 100644 packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index 7315e03b62a..c0f3ab023cb 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -1,4 +1,4 @@ -import { FormLabel } from '@mui/material'; +import { Box, FormLabel } from '@mui/material'; import * as React from 'react'; import { FormControl } from 'src/components/FormControl'; @@ -78,13 +78,17 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { - handleChange(e)} - /> - + + handleChange(e)} + /> + + ({ })); export const KubeClusterControlPlaneACL = React.memo((props: Props) => { - const theme = useTheme(); const { cluster, handleOpenDrawer, setControlPlaneACLMigrated } = props; const { classes } = useStyles(); @@ -95,22 +92,6 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { return isErrorKubernetesACL; }; - const IPACLdClusterToolTip = () => { - return ( - - ); - }; - const EnabledCopy = () => { return ( <> @@ -139,10 +120,7 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { return ( <> - - Cluster Requires Migration - - + Install ); @@ -150,7 +128,7 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { const kubeSpecsLeft = [ isLoadingKubernetesACL ? ( - + ) : failedMigrationStatus() ? ( ) : enabledACL ? ( diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 9e94bf121e9..6d1f08edb2e 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -18,7 +18,7 @@ import { import { Checkbox } from 'src/components/Checkbox'; import { TextField } from 'src/components/TextField'; import { Stack, Divider } from '@mui/material'; -import { StyledButton } from 'src/components/CheckoutBar/styles'; +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; import { Notice } from 'src/components/Notice/Notice'; @@ -69,6 +69,8 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const [controlPlaneACL, setControlPlaneACL] = React.useState(false); const [revisionID, setRevisionID] = React.useState(); + const [submitButtonLabel, setSubmitButtonLabel] = React.useState(''); + // refetchOnMount isnt good enough for this query because // it is already mounted in the rendered Drawer React.useEffect(() => { @@ -86,6 +88,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { setIPV4InputError(undefined); setIPV6InputError(undefined); setUpdating(false); + setSubmitButtonLabel(clusterMigrated ? 'Update' : 'Install'); refetchKubernetesACL(); } }, [open]); @@ -246,10 +249,6 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { ); }; - const SubmitButtonCopy = () => { - return clusterMigrated ? 'Update' : 'Install'; - }; - return ( { title="IPv6 Addresses or CIDR" error={ipV6InputError} /> - - - - + + diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx new file mode 100644 index 00000000000..f59d4fb895c --- /dev/null +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -0,0 +1,201 @@ +// This component was built asuming an unmodified MUI +import Table from '@mui/material/Table'; +import Grid from '@mui/material/Unstable_Grid2'; +import { styled } from '@mui/material/styles'; +import { Theme } from '@mui/material/styles'; +import { Link } from 'react-router-dom'; + +import { Box } from 'src/components/Box'; +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { Typography } from 'src/components/Typography'; + +// --------------------------------------------------------------------- +// Header Styles +// --------------------------------------------------------------------- + +export const StyledLink = styled(Link, { label: 'StyledLink' })( + ({ theme }) => ({ + '&:hover': { + color: theme.palette.primary.light, + textDecoration: 'underline', + }, + marginLeft: theme.spacing(), + }) +); + +// --------------------------------------------------------------------- +// Body Styles +// --------------------------------------------------------------------- + +export const StyledBodyGrid = styled(Grid, { label: 'StyledBodyGrid' })( + ({ theme }) => ({ + justifyContent: 'space-between', + padding: theme.spacing(2), + }) +); + +export const StyledColumnLabelGrid = styled(Grid, { + label: 'StyledColumnLabelGrid', +})(({ theme }) => ({ + color: theme.textColors.headlineStatic, + fontFamily: theme.font.bold, +})); + +export const StyledSummaryGrid = styled(Grid, { label: 'StyledSummaryGrid' })( + ({ theme }) => ({ + '& p': { + color: theme.textColors.tableStatic, + }, + }) +); + +export const StyledVPCBox = styled(Box, { label: 'StyledVPCBox' })( + ({ theme }) => ({ + padding: 0, + [theme.breakpoints.down('md')]: { + paddingBottom: theme.spacing(0.5), + }, + }) +); + +export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ + alignItems: 'center', + display: 'flex', + [theme.breakpoints.down('md')]: { + alignItems: 'flex-start', + flexDirection: 'column', + }, +})); + +export const StyledLabelBox = styled(Box, { label: 'StyledLabelBox' })( + ({ theme }) => ({ + fontFamily: theme.font.bold, + marginRight: '4px', + }) +); + +export const sxListItemMdBp = { + borderRight: 0, + flex: '50%', + padding: 0, +}; + +export const sxLastListItem = { + borderRight: 0, + paddingRight: 0, +}; + +export const StyledListItem = styled(Typography, { label: 'StyledTypography' })( + ({ theme }) => ({ + borderRight: `1px solid ${theme.borderColors.borderTypography}`, + color: theme.textColors.tableStatic, + display: 'flex', + padding: `0px 10px`, + [theme.breakpoints.down('md')]: { + ...sxListItemMdBp, + }, + }) +); + +export const sxListItemFirstChild = (theme: Theme) => ({ + [theme.breakpoints.down('md')]: { + ...sxListItemMdBp, + '&:first-of-type': { + paddingBottom: theme.spacing(0.5), + }, + }, +}); + +// --------------------------------------------------------------------- +// AccessTable Styles +// --------------------------------------------------------------------- + +export const StyledTable = styled(Table, { label: 'StyledTable' })( + ({ theme }) => ({ + '& td': { + border: 'none', + borderBottom: `1px solid ${theme.bg.bgPaper}`, + fontSize: '0.875rem', + lineHeight: 1, + whiteSpace: 'nowrap', + }, + '& th': { + backgroundColor: + theme.name === 'light' ? theme.color.grey10 : theme.bg.app, + borderBottom: `1px solid ${theme.bg.bgPaper}`, + color: theme.textColors.textAccessTable, + fontFamily: theme.font.bold, + fontSize: '0.875rem', + lineHeight: 1, + padding: theme.spacing(), + textAlign: 'left', + whiteSpace: 'nowrap', + width: 170, + }, + '& tr': { + height: 32, + }, + border: 'none', + tableLayout: 'fixed', + }) +); + +export const StyledTableGrid = styled(Grid, { label: 'StyledTableGrid' })( + ({ theme }) => ({ + '&.MuiGrid-item': { + padding: 0, + paddingLeft: theme.spacing(), + }, + }) +); + +export const StyledTableCell = styled(TableCell, { label: 'StyledTableCell' })( + ({ theme }) => ({ + '& div': { + fontSize: 15, + }, + alignItems: 'center', + backgroundColor: theme.bg.bgAccessRow, + color: theme.textColors.tableStatic, + display: 'flex', + fontFamily: '"UbuntuMono", monospace, sans-serif', + justifyContent: 'space-between', + padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, + position: 'relative', + }) +); + +export const StyledCopyTooltip = styled(CopyTooltip, { + label: 'StyledCopyTooltip', +})({ + '& svg': { + height: `12px`, + opacity: 0, + width: `12px`, + }, +}); + +export const StyledGradientDiv = styled('div', { label: 'StyledGradientDiv' })( + ({ theme }) => ({ + '&:after': { + backgroundImage: `linear-gradient(to right, ${theme.bg.bgAccessRowTransparentGradient}, ${theme.bg.bgAccessRow});`, + bottom: 0, + content: '""', + height: '100%', + position: 'absolute', + right: 0, + width: 30, + }, + overflowX: 'auto', + overflowY: 'hidden', // For Edge + paddingRight: 15, + }) +); + +export const StyledTableRow = styled(TableRow, { label: 'StyledTableRow' })({ + '&:hover .copy-tooltip > svg, & .copy-tooltip:focus > svg': { + opacity: 1, + }, +}); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index 7c029d208ae..c9dbe3855f8 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -33,6 +33,13 @@ import { KubeControlPlaneACLDrawer } from './KubeControlPaneACLDrawer'; import type { KubernetesCluster } from '@linode/api-v4/lib/kubernetes'; import type { Theme } from '@mui/material/styles'; +import { + StyledBox, + StyledLabelBox, + StyledListItem, + sxListItemFirstChild, +} from './KubeSummaryPanel.styles'; + const useStyles = makeStyles()((theme: Theme) => ({ actionRow: { '& button': { @@ -255,33 +262,47 @@ export const KubeSummaryPanel = React.memo((props: Props) => { } footer={ - <> - - IPACL - - - setControlPlaneACLDrawerOpen(true)} - /> - - + + + + IPACL: {' '} + + setControlPlaneACLDrawerOpen(true)} + /> + + + + } noBodyBottomBorder /> From 9ead96e16d65636706c8a6f07e7f1f211547a9f3 Mon Sep 17 00:00:00 2001 From: Talmai Oliveira Date: Wed, 25 Sep 2024 23:32:18 -0400 Subject: [PATCH 04/36] [LKEAPIFW-428] Additional UI tweaks --- .../CreateCluster/ControlPlaneACLPane.tsx | 76 +++++++++---------- .../CreateCluster/CreateCluster.tsx | 41 ++++++---- .../KubeClusterControlPlaneACL.tsx | 10 ++- .../KubeControlPaneACLDrawer.tsx | 11 +-- .../KubeSummaryPanel.styles.tsx | 1 - 5 files changed, 73 insertions(+), 66 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index c0f3ab023cb..e503a848f32 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -6,8 +6,8 @@ import { Link } from 'src/components/Link'; import { Typography } from 'src/components/Typography'; import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; import { ExtendedIP, validateIPs } from 'src/utilities/ipUtils'; -import { Notice } from 'src/components/Notice/Notice'; -import { Checkbox } from 'src/components/Checkbox'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { Toggle } from 'src/components/Toggle/Toggle'; import Stack from '@mui/material/Stack'; export interface ControlPlaneACLProps { @@ -17,17 +17,14 @@ export interface ControlPlaneACLProps { handleIPv4Change: (ips: ExtendedIP[]) => void; ipV6Addr: ExtendedIP[]; handleIPv6Change: (ips: ExtendedIP[]) => void; - aclError?: string; } export const IPACLCopy = () => ( - This is the text for Control Plane Access Control. -
+ This is the text for Control Plane Access Control.{' '} - Learn more about the control plane access control list + Learn more. - .
); @@ -39,13 +36,26 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { handleIPv4Change, ipV6Addr, handleIPv6Change, - aclError, } = props; const handleChange = (e: React.ChangeEvent) => { setControlPlaneACL(!enableControlPlaneACL); }; + const handleIPv4ChangeCB = React.useCallback( + (_ips: ExtendedIP[]) => { + handleIPv4Change(_ips); + }, + [handleIPv4Change] + ); + + const handleIPv6ChangeCB = React.useCallback( + (_ips: ExtendedIP[]) => { + handleIPv6Change(_ips); + }, + [handleIPv6Change] + ); + const statusStyle = (status: boolean) => { switch (status) { case false: @@ -55,16 +65,8 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { } }; - const [ipV4InputError, setIPV4InputError] = React.useState< - string | undefined - >(''); - const [ipV6InputError, setIPV6InputError] = React.useState< - string | undefined - >(''); - return ( - {aclError && } ({ '&&.MuiFormLabel-root.Mui-focused': { @@ -78,12 +80,16 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { - - handleChange(e)} + + handleChange(e)} + /> + } + label={'Enable IPACL'} /> { { + onChange={handleIPv4ChangeCB} + onBlur={(_ips: ExtendedIP[]) => { const validatedIPs = validateIPs(_ips, { allowEmptyAddress: false, errorMessage: 'Must be a valid IPv4 address.', }); - const ipsWithErrors = validatedIPs.filter((thisIP) => - setIPV4InputError(thisIP.error) - ); - if (ipsWithErrors.length === 0) { - handleIPv4Change(validatedIPs); - } + handleIPv4ChangeCB(validatedIPs); }} placeholder="0.0.0.0/0" - title="IPv4 Addresses or CIDR" - error={ipV4InputError} + title="IPv4 Addresses or CIDRs" /> { + onChange={handleIPv6ChangeCB} + onBlur={(_ips: ExtendedIP[]) => { const validatedIPs = validateIPs(_ips, { allowEmptyAddress: false, errorMessage: 'Must be a valid IPv6 address.', }); - const ipsWithErrors: ExtendedIP[] = validatedIPs.filter((thisIP) => - setIPV6InputError(thisIP.error) - ); - if (ipsWithErrors.length === 0) { - handleIPv6Change(validatedIPs); - } + handleIPv6Change(validatedIPs); }} placeholder="::/0" - title="IPv6 Addresses or CIDR" - error={ipV6InputError} + title="IPv6 Addresses or CIDRs" /> diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 8c35a735cc2..27ef4dd2cba 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -52,7 +52,7 @@ import { import { HAControlPlane } from './HAControlPlane'; import { ControlPlaneACLPane } from './ControlPlaneACLPane'; import { NodePoolPanel } from './NodePoolPanel'; -import { ExtendedIP, stringToExtendedIP } from 'src/utilities/ipUtils'; +import { ExtendedIP } from 'src/utilities/ipUtils'; import type { CreateKubeClusterPayload, @@ -83,12 +83,8 @@ export const CreateCluster = () => { const { data: account } = useAccount(); const { showHighAvailability } = getKubeHighAvailability(account); const { showControlPlaneACL } = getKubeControlPlaneACL(account); - const [ipV4Addr, setIPv4Addr] = React.useState([ - stringToExtendedIP('0.0.0.0/0'), - ]); - const [ipV6Addr, setIPv6Addr] = React.useState([ - stringToExtendedIP('::/0'), - ]); + const [ipV4Addr, setIPv4Addr] = React.useState([]); + const [ipV6Addr, setIPv6Addr] = React.useState([]); const { data: kubernetesHighAvailabilityTypesData, @@ -139,13 +135,25 @@ export const CreateCluster = () => { pick(['type', 'count']) ) as CreateNodePoolData[]; - const _ipv4 = ipV4Addr.map((ip) => { - return ip.address; - }); + const _ipv4 = ipV4Addr + .map((ip) => { + return ip.address; + }) + .filter((ip) => ip != ''); + + const _ipv6 = ipV6Addr + .map((ip) => { + return ip.address; + }) + .filter((ip) => ip != ''); - const _ipv6 = ipV6Addr.map((ip) => { - return ip.address; - }); + const addressIPv4Payload = { + ...(_ipv4.length > 0 && { ipv4: _ipv4 }), + }; + + const addressIPv6Payload = { + ...(_ipv6.length > 0 && { ipv6: _ipv6 }), + }; const payload: CreateKubeClusterPayload = { control_plane: { @@ -153,7 +161,12 @@ export const CreateCluster = () => { acl: { enabled: controlPlaneACL, 'revision-id': '', - addresses: { ipv4: _ipv4, ipv6: _ipv6 }, + ...((_ipv4.length > 0 || _ipv6.length > 0) && { + addresses: { + ...addressIPv4Payload, + ...addressIPv6Payload, + }, + }), }, }, k8s_version: version, diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx index 83276268f05..6eb03981aa1 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx @@ -109,7 +109,7 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { <> - IPACL Disabled + Enable IPACL @@ -120,13 +120,15 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { return ( <> - Install + + Install IPACL + ); }; - const kubeSpecsLeft = [ + const availableActions = [ isLoadingKubernetesACL ? ( ) : failedMigrationStatus() ? ( @@ -147,7 +149,7 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { className={classes.item} wrap="nowrap" > - {kubeSpecsLeft} + {availableActions} ); }); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 6d1f08edb2e..888c013e137 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -165,7 +165,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { <> IPACL is not yet installed on this cluster.... may take up to 10 - minutes before.. + minutes or more, before ACLs are enforced... @@ -206,7 +206,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { value={revisionID} onBlur={(e) => setRevisionID(e.target.value)} /> - + ); } else { @@ -264,9 +264,6 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { > - - {clusterLabel} - When a cluster is equipped with an ACL, the apiserver and @@ -275,7 +272,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { - + @@ -290,7 +287,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { } }} /> - + diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index f59d4fb895c..cc035598a43 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -89,7 +89,6 @@ export const sxLastListItem = { export const StyledListItem = styled(Typography, { label: 'StyledTypography' })( ({ theme }) => ({ - borderRight: `1px solid ${theme.borderColors.borderTypography}`, color: theme.textColors.tableStatic, display: 'flex', padding: `0px 10px`, From 1eec5c01ba0ae34788a1df54dea4ac29e1f1f743 Mon Sep 17 00:00:00 2001 From: Talmai Oliveira Date: Thu, 26 Sep 2024 08:39:39 -0400 Subject: [PATCH 05/36] [LKEAPIFW-428] Substituition of UI components --- .../CreateCluster/ControlPlaneACLPane.tsx | 73 +++++------ .../KubeControlPaneACLDrawer.tsx | 118 +++++++++++------- 2 files changed, 103 insertions(+), 88 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index e503a848f32..d831a856ce9 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -56,15 +56,6 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { [handleIPv6Change] ); - const statusStyle = (status: boolean) => { - switch (status) { - case false: - return 'none'; - default: - return ''; - } - }; - return ( { label={'Enable IPACL'} /> - - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }); - handleIPv4ChangeCB(validatedIPs); - }} - placeholder="0.0.0.0/0" - title="IPv4 Addresses or CIDRs" - /> - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv6 address.', - }); - handleIPv6Change(validatedIPs); - }} - placeholder="::/0" - title="IPv6 Addresses or CIDRs" - /> - + {enableControlPlaneACL && ( + + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }); + handleIPv4ChangeCB(validatedIPs); + }} + placeholder="0.0.0.0/0" + title="IPv4 Addresses or CIDRs" + /> + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv6 address.', + }); + handleIPv6ChangeCB(validatedIPs); + }} + placeholder="::/0" + title="IPv6 Addresses or CIDRs" + /> + + )} ); }; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 888c013e137..70eb9cce941 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -15,12 +15,14 @@ import { validateIPs, stringToExtendedIP, } from 'src/utilities/ipUtils'; -import { Checkbox } from 'src/components/Checkbox'; import { TextField } from 'src/components/TextField'; import { Stack, Divider } from '@mui/material'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; import { Notice } from 'src/components/Notice/Notice'; +import { Box } from '@mui/material'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { Toggle } from 'src/components/Toggle/Toggle'; interface Props { closeDrawer: () => void; @@ -33,12 +35,6 @@ interface Props { export const KubeControlPlaneACLDrawer = (props: Props) => { const { closeDrawer, clusterId, clusterLabel, clusterMigrated, open } = props; - const [ipV4InputError, setIPV4InputError] = React.useState< - string | undefined - >(''); - const [ipV6InputError, setIPV6InputError] = React.useState< - string | undefined - >(''); const [updateError, setUpdateACLError] = React.useState(); const [updating, setUpdating] = React.useState(false); @@ -85,8 +81,6 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { setControlPlaneACL(_enabled ? _enabled : false); setRevisionID(_revisionID ? _revisionID : ''); setUpdateACLError(isErrorKubernetesACL?.[0].reason); - setIPV4InputError(undefined); - setIPV6InputError(undefined); setUpdating(false); setSubmitButtonLabel(clusterMigrated ? 'Update' : 'Install'); refetchKubernetesACL(); @@ -105,19 +99,36 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { setUpdateACLError(undefined); setUpdating(true); - const _newIPv4 = ipV4Addr.map((ip) => { - return ip.address; - }); + const _ipv4 = ipV4Addr + .map((ip) => { + return ip.address; + }) + .filter((ip) => ip != ''); + + const _ipv6 = ipV6Addr + .map((ip) => { + return ip.address; + }) + .filter((ip) => ip != ''); - const _newIPv6 = ipV6Addr.map((ip) => { - return ip.address; - }); + const addressIPv4Payload = { + ...(_ipv4.length > 0 && { ipv4: _ipv4 }), + }; + + const addressIPv6Payload = { + ...(_ipv6.length > 0 && { ipv6: _ipv6 }), + }; const payload: KubernetesControlPlaneACLPayload = { acl: { enabled: clusterMigrated ? controlPlaneACL : true, // new cluster installations default to true 'revision-id': revisionID, - addresses: { ipv4: _newIPv4, ipv6: _newIPv6 }, + ...((_ipv4.length > 0 || _ipv6.length > 0) && { + addresses: { + ...addressIPv4Payload, + ...addressIPv6Payload, + }, + }), }, }; @@ -148,6 +159,20 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { } }; + const handleIPv4ChangeCB = React.useCallback( + (_ips: ExtendedIP[]) => { + setIPv4Addr(_ips); + }, + [setIPv4Addr] + ); + + const handleIPv6ChangeCB = React.useCallback( + (_ips: ExtendedIP[]) => { + setIPv6Addr(_ips); + }, + [setIPv6Addr] + ); + const ErrorMessage = () => { if (!!updateError && clusterMigrated) { return ( @@ -276,18 +301,28 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { - - { - if (clusterMigrated) { - return setControlPlaneACL(e.target.checked); - } - }} - /> - + {clusterMigrated && ( + <> + + + { + if (clusterMigrated) { + return setControlPlaneACL(e.target.checked); + } + }} + /> + } + label={'IPACL Enabled'} + /> + + + + )} @@ -295,46 +330,35 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { { + onChange={handleIPv4ChangeCB} + onBlur={(_ips: ExtendedIP[]) => { const validatedIPs = validateIPs(_ips, { allowEmptyAddress: false, errorMessage: 'Must be a valid IPv4 address.', }); - const ipsWithErrors: ExtendedIP[] = validatedIPs.filter( - (thisIP) => setIPV4InputError(thisIP.error) - ); - if (ipsWithErrors.length === 0) { - setIPv4Addr(validatedIPs); - } + handleIPv4ChangeCB(validatedIPs); }} placeholder="0.0.0.0/0" - title="IPv4 Addresses or CIDR" - error={ipV4InputError} + title="IPv4 Addresses or CIDRs" /> { + onChange={handleIPv6ChangeCB} + onBlur={(_ips: ExtendedIP[]) => { const validatedIPs = validateIPs(_ips, { allowEmptyAddress: false, errorMessage: 'Must be a valid IPv6 address.', }); - const ipsWithErrors: ExtendedIP[] = validatedIPs.filter( - (thisIP) => setIPV6InputError(thisIP.error) - ); - if (ipsWithErrors.length === 0) { - setIPv6Addr(validatedIPs); - } + handleIPv6ChangeCB(validatedIPs); }} placeholder="::/0" - title="IPv6 Addresses or CIDR" - error={ipV6InputError} + title="IPv6 Addresses or CIDRs" /> Date: Mon, 30 Sep 2024 00:33:29 -0400 Subject: [PATCH 06/36] [LKEAPIFW-428] Another round of UI tweaks: multiline IP default values; IPACL drawer showing enabled only when enabled wip --- .../CreateCluster/ControlPlaneACLPane.tsx | 4 +-- .../CreateCluster/CreateCluster.tsx | 10 ++++-- .../KubeClusterControlPlaneACL.tsx | 2 +- .../KubeControlPaneACLDrawer.tsx | 32 ++++++++++--------- .../KubeSummaryPanel.styles.tsx | 2 +- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index d831a856ce9..352a6ff9afd 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -71,7 +71,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { - + { /> {enableControlPlaneACL && ( - + { const { data: account } = useAccount(); const { showHighAvailability } = getKubeHighAvailability(account); const { showControlPlaneACL } = getKubeControlPlaneACL(account); - const [ipV4Addr, setIPv4Addr] = React.useState([]); - const [ipV6Addr, setIPv6Addr] = React.useState([]); + const [ipV4Addr, setIPv4Addr] = React.useState([ + stringToExtendedIP(''), + ]); + const [ipV6Addr, setIPv6Addr] = React.useState([ + stringToExtendedIP(''), + ]); const { data: kubernetesHighAvailabilityTypesData, diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx index 6eb03981aa1..24bd73aca87 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx @@ -130,7 +130,7 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { const availableActions = [ isLoadingKubernetesACL ? ( - + ) : failedMigrationStatus() ? ( ) : enabledACL ? ( diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 70eb9cce941..07e05732d58 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -59,6 +59,8 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const _revisionID = data?.acl?.['revision-id']; + const _hideEnableFromUI = !clusterMigrated || !_enabled; + // respective react states const [ipV4Addr, setIPv4Addr] = React.useState([]); const [ipV6Addr, setIPv6Addr] = React.useState([]); @@ -72,17 +74,19 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { React.useEffect(() => { if (open && !isLoadingKubernetesACL && !isFetchingKubernetesACL) { // updates states based on queried data - setIPv4Addr( - _ipv4 ? _ipv4 : clusterMigrated ? [] : [stringToExtendedIP('0.0.0.0/0')] - ); - setIPv6Addr( - _ipv6 ? _ipv6 : clusterMigrated ? [] : [stringToExtendedIP('::/0')] - ); + setIPv4Addr(_ipv4 ? _ipv4 : [stringToExtendedIP('')]); + setIPv6Addr(_ipv6 ? _ipv6 : [stringToExtendedIP('')]); setControlPlaneACL(_enabled ? _enabled : false); setRevisionID(_revisionID ? _revisionID : ''); setUpdateACLError(isErrorKubernetesACL?.[0].reason); setUpdating(false); - setSubmitButtonLabel(clusterMigrated ? 'Update' : 'Install'); + setSubmitButtonLabel( + _hideEnableFromUI + ? 'Enable IPACL' + : clusterMigrated + ? 'Update IPACL' + : 'Install IPACL' + ); refetchKubernetesACL(); } }, [open]); @@ -121,7 +125,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const payload: KubernetesControlPlaneACLPayload = { acl: { - enabled: clusterMigrated ? controlPlaneACL : true, // new cluster installations default to true + enabled: _hideEnableFromUI ? true : controlPlaneACL, // both new cluster installations as well as all the states where the UI disabled the option for the user to enable, we default to true 'revision-id': revisionID, ...((_ipv4.length > 0 || _ipv6.length > 0) && { addresses: { @@ -188,11 +192,10 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { if (!clusterMigrated) { return ( <> - + IPACL is not yet installed on this cluster.... may take up to 10 minutes or more, before ACLs are enforced... - ); } else { @@ -264,7 +267,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { Addresses - + A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW access to the control plane. @@ -299,9 +302,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { - - - {clusterMigrated && ( + {!_hideEnableFromUI && ( <> @@ -327,6 +328,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { + { secondaryButtonProps={{ label: 'Cancel', onClick: closeDrawer }} /> - + diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index cc035598a43..6fc60b1dde3 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -72,7 +72,7 @@ export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ export const StyledLabelBox = styled(Box, { label: 'StyledLabelBox' })( ({ theme }) => ({ fontFamily: theme.font.bold, - marginRight: '4px', + marginRight: '8px', }) ); From d41ff96255857cf22467b2697e7f00ab4629ad79 Mon Sep 17 00:00:00 2001 From: Hana Xu Date: Tue, 1 Oct 2024 11:54:48 -0400 Subject: [PATCH 07/36] fix multiple ip css issues --- .../CreateCluster/ControlPlaneACLPane.tsx | 36 +++++------ .../KubeControlPaneACLDrawer.tsx | 62 +++++++++---------- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index 352a6ff9afd..736db8f4cc3 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -57,21 +57,19 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { ); return ( - - ({ - '&&.MuiFormLabel-root.Mui-focused': { - color: theme.name === 'dark' ? 'white' : theme.color.black, - }, - })} - id="ipacl-radio-buttons-group-label" - > - - Control Plane Access Control (IPACL) + <> + + + + Control Plane Access Control (IPACL) + + + + This is the text for Control Plane Access Control.{' '} + + Learn more. + - - - { onChange={(e) => handleChange(e)} /> } - label={'Enable IPACL'} + label="Enable IPACL" /> - + {enableControlPlaneACL && ( - + { placeholder="::/0" title="IPv6 Addresses or CIDRs" /> - + )} - + ); }; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 07e05732d58..f9239bf1a82 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -324,40 +324,39 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} - - - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }); - handleIPv4ChangeCB(validatedIPs); - }} - placeholder="0.0.0.0/0" - title="IPv4 Addresses or CIDRs" - /> - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv6 address.', - }); - handleIPv6ChangeCB(validatedIPs); - }} - placeholder="::/0" - title="IPv6 Addresses or CIDRs" - /> - + + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }); + handleIPv4ChangeCB(validatedIPs); + }} + placeholder="0.0.0.0/0" + title="IPv4 Addresses or CIDRs" + /> + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv6 address.', + }); + handleIPv6ChangeCB(validatedIPs); + }} + placeholder="::/0" + title="IPv6 Addresses or CIDRs" + /> + { }} secondaryButtonProps={{ label: 'Cancel', onClick: closeDrawer }} /> - From 265a3cac09776ec4c5ba958d4c301dac5813c151 Mon Sep 17 00:00:00 2001 From: Talmai Oliveira Date: Wed, 2 Oct 2024 16:02:44 -0400 Subject: [PATCH 08/36] [LKEAPIFW-428] Copy text adjustment --- .../Kubernetes/CreateCluster/ControlPlaneACLPane.tsx | 1 - .../KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index 736db8f4cc3..c2749586b9c 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -8,7 +8,6 @@ import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput' import { ExtendedIP, validateIPs } from 'src/utilities/ipUtils'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { Toggle } from 'src/components/Toggle/Toggle'; -import Stack from '@mui/material/Stack'; export interface ControlPlaneACLProps { enableControlPlaneACL: boolean; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index f9239bf1a82..0394121a27f 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -193,8 +193,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { return ( <> - IPACL is not yet installed on this cluster.... may take up to 10 - minutes or more, before ACLs are enforced... + IPACL has not yet been installed on this cluster. During + installation, it may take up to 20 minutes before ACLs are fully + enforced for the first time. ); From bc7af8a517f39464416f3b969cf57eb5c516da78 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 2 Oct 2024 16:46:34 -0400 Subject: [PATCH 09/36] add changeset --- packages/manager/.changeset/pr-10968-added-1727901904107.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10968-added-1727901904107.md diff --git a/packages/manager/.changeset/pr-10968-added-1727901904107.md b/packages/manager/.changeset/pr-10968-added-1727901904107.md new file mode 100644 index 00000000000..f4e98f52b9a --- /dev/null +++ b/packages/manager/.changeset/pr-10968-added-1727901904107.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +IP ACL integration to LKE clusters ([#10968](https://github.com/linode/manager/pull/10968)) From 903ce4586f099ae007edba746cdcd93727a35fe2 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 3 Oct 2024 10:46:51 -0400 Subject: [PATCH 10/36] Added changeset: ACL related endpoints and types for LKE clusters --- packages/api-v4/.changeset/pr-10968-added-1727966811522.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-10968-added-1727966811522.md diff --git a/packages/api-v4/.changeset/pr-10968-added-1727966811522.md b/packages/api-v4/.changeset/pr-10968-added-1727966811522.md new file mode 100644 index 00000000000..df179fea56e --- /dev/null +++ b/packages/api-v4/.changeset/pr-10968-added-1727966811522.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +ACL related endpoints and types for LKE clusters ([#10968](https://github.com/linode/manager/pull/10968)) From 2bb302b9690ef3037bbaae27906d3dc8fb8aa5c5 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Fri, 4 Oct 2024 13:44:41 -0400 Subject: [PATCH 11/36] fix small eslint warnings + update spacing/design review with Daniel --- .../MultipleIPInput/MultipleIPInput.tsx | 38 ++--- .../CreateCluster/ControlPlaneACLPane.tsx | 58 +++---- .../CreateCluster/CreateCluster.tsx | 50 +++--- .../KubeClusterControlPlaneACL.tsx | 80 +++++----- .../KubeControlPaneACLDrawer.tsx | 142 +++++++++--------- .../KubeSummaryPanel.styles.tsx | 7 +- .../KubeSummaryPanel.tsx | 76 +++++----- 7 files changed, 226 insertions(+), 225 deletions(-) diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 6d241a3b2df..00d0022ea4a 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -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'; @@ -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: { @@ -66,6 +67,7 @@ interface Props { helperText?: string; inputProps?: InputBaseProps; ips: ExtendedIP[]; + isLinkStyled?: boolean; onBlur?: (ips: ExtendedIP[]) => void; onChange: (ips: ExtendedIP[]) => void; placeholder?: string; @@ -83,6 +85,7 @@ export const MultipleIPInput = React.memo((props: Props) => { forVPCIPv4Ranges, helperText, ips, + isLinkStyled, onBlur, onChange, placeholder, @@ -128,20 +131,21 @@ export const MultipleIPInput = React.memo((props: Props) => { return null; } - const addIPButton = forVPCIPv4Ranges ? ( - - {buttonText} - - ) : ( - - ); + const addIPButton = + forVPCIPv4Ranges || isLinkStyled ? ( + + {buttonText} + + ) : ( + + ); return (
diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index c2749586b9c..91423488726 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -2,20 +2,22 @@ import { Box, FormLabel } from '@mui/material'; import * as React from 'react'; import { FormControl } from 'src/components/FormControl'; +import { FormControlLabel } from 'src/components/FormControlLabel'; import { Link } from 'src/components/Link'; -import { Typography } from 'src/components/Typography'; import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; -import { ExtendedIP, validateIPs } from 'src/utilities/ipUtils'; -import { FormControlLabel } from 'src/components/FormControlLabel'; 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; - setControlPlaneACL: (enabled: boolean) => void; - ipV4Addr: ExtendedIP[]; handleIPv4Change: (ips: ExtendedIP[]) => void; - ipV6Addr: ExtendedIP[]; handleIPv6Change: (ips: ExtendedIP[]) => void; + ipV4Addr: ExtendedIP[]; + ipV6Addr: ExtendedIP[]; + setControlPlaneACL: (enabled: boolean) => void; } export const IPACLCopy = () => ( @@ -30,11 +32,11 @@ export const IPACLCopy = () => ( export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { const { enableControlPlaneACL, - setControlPlaneACL, - ipV4Addr, handleIPv4Change, - ipV6Addr, handleIPv6Change, + ipV4Addr, + ipV6Addr, + setControlPlaneACL, } = props; const handleChange = (e: React.ChangeEvent) => { @@ -83,9 +85,6 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { {enableControlPlaneACL && ( { const validatedIPs = validateIPs(_ips, { allowEmptyAddress: false, @@ -93,23 +92,30 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { }); handleIPv4ChangeCB(validatedIPs); }} + buttonText="Add IPv4 Address" + ips={ipV4Addr} + isLinkStyled + onChange={handleIPv4ChangeCB} placeholder="0.0.0.0/0" title="IPv4 Addresses or CIDRs" /> - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv6 address.', - }); - handleIPv6ChangeCB(validatedIPs); - }} - placeholder="::/0" - title="IPv6 Addresses or CIDRs" - /> + + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv6 address.', + }); + handleIPv6ChangeCB(validatedIPs); + }} + buttonText="Add IPv6 Address" + ips={ipV6Addr} + isLinkStyled + onChange={handleIPv6ChangeCB} + placeholder="::/0" + title="IPv6 Addresses or CIDRs" + /> + )} diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index dbc04001ebf..7898f3aba79 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -37,22 +37,22 @@ import { useAllTypes } from 'src/queries/types'; import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils'; import { extendType } from 'src/utilities/extendType'; import { filterCurrentTypes } from 'src/utilities/filterCurrentLinodeTypes'; +import { stringToExtendedIP } from 'src/utilities/ipUtils'; import { plansNoticesUtils } from 'src/utilities/planNotices'; -import { DOCS_LINK_LABEL_DC_PRICING } from 'src/utilities/pricing/constants'; import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; +import { DOCS_LINK_LABEL_DC_PRICING } from 'src/utilities/pricing/constants'; import { getDCSpecificPriceByType } from 'src/utilities/pricing/dynamicPricing'; import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; import KubeCheckoutBar from '../KubeCheckoutBar'; +import { ControlPlaneACLPane } from './ControlPlaneACLPane'; import { StyledDocsLinkContainer, StyledRegionSelectStack, useStyles, } from './CreateCluster.styles'; import { HAControlPlane } from './HAControlPlane'; -import { ControlPlaneACLPane } from './ControlPlaneACLPane'; import { NodePoolPanel } from './NodePoolPanel'; -import { ExtendedIP, stringToExtendedIP } from 'src/utilities/ipUtils'; import type { CreateKubeClusterPayload, @@ -60,6 +60,7 @@ import type { KubeNodePoolResponse, } from '@linode/api-v4/lib/kubernetes'; import type { APIError } from '@linode/api-v4/lib/types'; +import type { ExtendedIP } from 'src/utilities/ipUtils'; export const CreateCluster = () => { const { classes } = useStyles(); @@ -161,7 +162,6 @@ export const CreateCluster = () => { const payload: CreateKubeClusterPayload = { control_plane: { - high_availability: highAvailability ?? false, acl: { enabled: controlPlaneACL, 'revision-id': '', @@ -172,6 +172,7 @@ export const CreateCluster = () => { }, }), }, + high_availability: highAvailability ?? false, }, k8s_version: version, label, @@ -309,7 +310,7 @@ export const CreateCluster = () => { value={versions.find((v) => v.value === version) ?? null} /> - {showHighAvailability ? ( + {showHighAvailability && ( { setHighAvailability={setHighAvailability} /> - ) : null} - - {showControlPlaneACL ? ( - - { - setIPv4Addr(newIpV4Addr); - }} - ipV6Addr={ipV6Addr} - handleIPv6Change={(newIpV6Addr: ExtendedIP[]) => { - setIPv6Addr(newIpV6Addr); - }} - /> - - ) : null} + )} + + {showControlPlaneACL && ( + <> + + + { + setIPv4Addr(newIpV4Addr); + }} + handleIPv6Change={(newIpV6Addr: ExtendedIP[]) => { + setIPv6Addr(newIpV6Addr); + }} + enableControlPlaneACL={controlPlaneACL} + ipV4Addr={ipV4Addr} + ipV6Addr={ipV6Addr} + setControlPlaneACL={setControlPlaneACL} + /> + + + )} void; handleOpenDrawer: () => void; + setControlPlaneACLMigrated: (s: boolean) => void; } const useStyles = makeStyles()((theme: Theme) => ({ + aclElement: { + '&:hover': { + opacity: 0.7, + }, + '&:last-child': { + borderRight: 'none', + }, + alignItems: 'center', + borderRight: '1px solid #c4c4c4', + cursor: 'pointer', + display: 'flex', + }, + aclElementText: { + color: theme.textColors.linkActiveLight, + marginRight: theme.spacing(1), + whiteSpace: 'nowrap', + }, iconTextOuter: { flexBasis: '72%', minWidth: 115, @@ -47,23 +64,6 @@ const useStyles = makeStyles()((theme: Theme) => ({ minWidth: 320, }, }, - aclElement: { - '&:hover': { - opacity: 0.7, - }, - '&:last-child': { - borderRight: 'none', - }, - alignItems: 'center', - borderRight: '1px solid #c4c4c4', - cursor: 'pointer', - display: 'flex', - }, - aclElementText: { - color: theme.textColors.linkActiveLight, - marginRight: theme.spacing(1), - whiteSpace: 'nowrap', - }, })); export const KubeClusterControlPlaneACL = React.memo((props: Props) => { @@ -77,7 +77,7 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { } = useKubernetesControlPlaneACLQuery(cluster.id); const enabledACL = acl_response ? acl_response.acl.enabled : false; - //const revisionIDACL = acl_response ? acl_response.acl['revision-id'] : ''; + // const revisionIDACL = acl_response ? acl_response.acl['revision-id'] : ''; const totalIPv4 = acl_response?.acl.addresses?.ipv4?.length ? acl_response?.acl.addresses?.ipv4?.length : 0; @@ -94,43 +94,35 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { const EnabledCopy = () => { return ( - <> - - - {pluralize('IP Address', 'IP Addresses', totalNumberIPs)} - - - + + + {pluralize('IP Address', 'IP Addresses', totalNumberIPs)} + + ); }; const DisabledCopy = () => { return ( - <> - - - Enable IPACL - - - + + Enable IPACL + ); }; const NotMigratedCopy = () => { return ( - <> - - - Install IPACL - - - + + + Install IPACL + + ); }; const availableActions = [ isLoadingKubernetesACL ? ( - + ) : failedMigrationStatus() ? ( ) : enabledACL ? ( @@ -142,11 +134,11 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { return ( {availableActions} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 0394121a27f..ea6fd7a87ef 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -1,28 +1,26 @@ +import { Divider, Stack } from '@mui/material'; +import { Box } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; import { DrawerContent } from 'src/components/DrawerContent'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; +import { Notice } from 'src/components/Notice/Notice'; +import { TextField } from 'src/components/TextField'; +import { Toggle } from 'src/components/Toggle/Toggle'; import { Typography } from 'src/components/Typography'; import { + useKubernetesClusterMutation, useKubernetesControlPlaneACLMutation, useKubernetesControlPlaneACLQuery, - useKubernetesClusterMutation, } from 'src/queries/kubernetes'; -import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; -import { - ExtendedIP, - validateIPs, - stringToExtendedIP, -} from 'src/utilities/ipUtils'; -import { TextField } from 'src/components/TextField'; -import { Stack, Divider } from '@mui/material'; -import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; -import { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; -import { Notice } from 'src/components/Notice/Notice'; -import { Box } from '@mui/material'; -import { FormControlLabel } from 'src/components/FormControlLabel'; -import { Toggle } from 'src/components/Toggle/Toggle'; +import { stringToExtendedIP, validateIPs } from 'src/utilities/ipUtils'; + +import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; +import type { ExtendedIP } from 'src/utilities/ipUtils'; interface Props { closeDrawer: () => void; @@ -41,8 +39,8 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const { data: data, error: isErrorKubernetesACL, - isLoading: isLoadingKubernetesACL, isFetching: isFetchingKubernetesACL, + isLoading: isLoadingKubernetesACL, refetch: refetchKubernetesACL, } = useKubernetesControlPlaneACLQuery(clusterId); @@ -191,13 +189,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const ClusterNeedsMigration = () => { if (!clusterMigrated) { return ( - <> - - IPACL has not yet been installed on this cluster. During - installation, it may take up to 20 minutes before ACLs are fully - enforced for the first time. - - + + IPACL has not yet been installed on this cluster. During installation, + it may take up to 20 minutes before ACLs are fully enforced for the + first time. + ); } else { return <>; @@ -206,21 +202,19 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const EnabledCopy = () => { return ( - <> - - - Enabled - - - - A value of true results in a default policy of DENY. A value of - false results in a default policy of ALLOW (i.e., access controls - are disabled). When enabled, control plane access controls can - only be accessible through the defined IP CIDRs. - - + + + Enabled - + + + A value of true results in a default policy of DENY. A value of + false results in a default policy of ALLOW (i.e., access controls + are disabled). When enabled, control plane access controls can only + be accessible through the defined IP CIDRs. + + + ); }; @@ -232,10 +226,10 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { setRevisionID(e.target.value)} + value={revisionID} /> - + ); } else { @@ -245,20 +239,18 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const RevisionIDCopy = () => { return ( - <> - - - Revision ID - - - - Enables clients to track events related to ACL update requests and - enforcements. Optional field. If omitted, defaults to a randomly - generated string. - - + + + Revision ID - + + + Enables clients to track events related to ACL update requests and + enforcements. Optional field. If omitted, defaults to a randomly + generated string. + + + ); }; @@ -301,7 +293,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { - + {!_hideEnableFromUI && ( <> @@ -310,19 +302,19 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { { if (clusterMigrated) { return setControlPlaneACL(e.target.checked); } }} + checked={clusterMigrated ? controlPlaneACL : true} + name="ipacl-checkbox" /> } label={'IPACL Enabled'} /> - + )} @@ -330,9 +322,6 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { { const validatedIPs = validateIPs(_ips, { allowEmptyAddress: false, @@ -340,23 +329,30 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { }); handleIPv4ChangeCB(validatedIPs); }} + buttonText="Add IPv4 Address" + ips={ipV4Addr} + isLinkStyled + onChange={handleIPv4ChangeCB} placeholder="0.0.0.0/0" title="IPv4 Addresses or CIDRs" /> - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv6 address.', - }); - handleIPv6ChangeCB(validatedIPs); - }} - placeholder="::/0" - title="IPv6 Addresses or CIDRs" - /> + + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv6 address.', + }); + handleIPv6ChangeCB(validatedIPs); + }} + buttonText="Add IPv6 Address" + ips={ipV6Addr} + isLinkStyled + onChange={handleIPv6ChangeCB} + placeholder="::/0" + title="IPv6 Addresses or CIDRs" + /> + +import { styled } from '@mui/material/styles'; import Table from '@mui/material/Table'; import Grid from '@mui/material/Unstable_Grid2'; -import { styled } from '@mui/material/styles'; -import { Theme } from '@mui/material/styles'; import { Link } from 'react-router-dom'; import { Box } from 'src/components/Box'; @@ -11,6 +10,8 @@ import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; +import type { Theme } from '@mui/material/styles'; + // --------------------------------------------------------------------- // Header Styles // --------------------------------------------------------------------- @@ -72,7 +73,7 @@ export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ export const StyledLabelBox = styled(Box, { label: 'StyledLabelBox' })( ({ theme }) => ({ fontFamily: theme.font.bold, - marginRight: '8px', + marginRight: theme.spacing(0.5), }) ); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index c9dbe3855f8..adcaad01a42 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -15,8 +15,8 @@ import { EntityHeader } from 'src/components/EntityHeader/EntityHeader'; import { Stack } from 'src/components/Stack'; import { TagCell } from 'src/components/TagCell/TagCell'; import { Typography } from 'src/components/Typography'; -import { KubeClusterSpecs } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs'; import { KubeClusterControlPlaneACL } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL'; +import { KubeClusterSpecs } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useKubernetesClusterMutation, @@ -29,10 +29,6 @@ import { DeleteKubernetesClusterDialog } from './DeleteKubernetesClusterDialog'; import { KubeConfigDisplay } from './KubeConfigDisplay'; import { KubeConfigDrawer } from './KubeConfigDrawer'; import { KubeControlPlaneACLDrawer } from './KubeControlPaneACLDrawer'; - -import type { KubernetesCluster } from '@linode/api-v4/lib/kubernetes'; -import type { Theme } from '@mui/material/styles'; - import { StyledBox, StyledLabelBox, @@ -40,6 +36,9 @@ import { sxListItemFirstChild, } from './KubeSummaryPanel.styles'; +import type { KubernetesCluster } from '@linode/api-v4/lib/kubernetes'; +import type { Theme } from '@mui/material/styles'; + const useStyles = makeStyles()((theme: Theme) => ({ actionRow: { '& button': { @@ -230,37 +229,6 @@ export const KubeSummaryPanel = React.memo((props: Props) => { } - header={ - - - Summary - - - { - window.open(dashboard?.url, '_blank'); - }} - className={classes.dashboard} - disabled={Boolean(dashboardError) || !dashboard} - > - Kubernetes Dashboard - - - setIsDeleteDialogOpen(true)} - > - Delete Cluster - - - - } footer={ { lg={8} xs={12} > - + { setControlPlaneACLDrawerOpen(true)} + setControlPlaneACLMigrated={setControlPlaneACLMigrated} /> } + header={ + + + Summary + + + { + window.open(dashboard?.url, '_blank'); + }} + className={classes.dashboard} + disabled={Boolean(dashboardError) || !dashboard} + > + Kubernetes Dashboard + + + setIsDeleteDialogOpen(true)} + > + Delete Cluster + + + + } noBodyBottomBorder /> From 27bbbbb5083f30db5a4a38af7d20891dfbd54920 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Fri, 4 Oct 2024 17:19:28 -0400 Subject: [PATCH 12/36] get rid of extra divider, will need to make a few more design updates --- .../src/features/Kubernetes/CreateCluster/CreateCluster.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 7898f3aba79..f07f0c72955 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -325,7 +325,6 @@ export const CreateCluster = () => { /> )} - {showControlPlaneACL && ( <> From 4af433ece35b4eeaf2d3ab23c466f525f9126bfd Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Mon, 7 Oct 2024 13:43:56 -0400 Subject: [PATCH 13/36] updates as per Daniel's feedback --- .../CreateCluster/ControlPlaneACLPane.tsx | 9 + .../CreateCluster/CreateCluster.tsx | 23 ++- .../KubeControlPaneACLDrawer.tsx | 183 ++++++++---------- 3 files changed, 103 insertions(+), 112 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index 91423488726..992b6639493 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -1,10 +1,12 @@ import { Box, FormLabel } from '@mui/material'; import * as React from 'react'; +import { ErrorMessage } from 'src/components/ErrorMessage'; import { FormControl } from 'src/components/FormControl'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { Link } from 'src/components/Link'; 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'; @@ -13,6 +15,7 @@ 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[]; @@ -32,6 +35,7 @@ export const IPACLCopy = () => ( export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { const { enableControlPlaneACL, + errorText, handleIPv4Change, handleIPv6Change, ipV4Addr, @@ -65,6 +69,11 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { Control Plane Access Control (IPACL) + {errorText && ( + + {' '} + + )} This is the text for Control Plane Access Control.{' '} diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index f07f0c72955..9aa27252c1d 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -165,12 +165,13 @@ export const CreateCluster = () => { acl: { enabled: controlPlaneACL, 'revision-id': '', - ...((_ipv4.length > 0 || _ipv6.length > 0) && { - addresses: { - ...addressIPv4Payload, - ...addressIPv6Payload, - }, - }), + ...(controlPlaneACL && // only send the IPs if we are enabling IPACL + (_ipv4.length > 0 || _ipv6.length > 0) && { + addresses: { + ...addressIPv4Payload, + ...addressIPv6Payload, + }, + }), }, high_availability: highAvailability ?? false, }, @@ -226,7 +227,14 @@ export const CreateCluster = () => { }); const errorMap = getErrorMap( - ['region', 'node_pools', 'label', 'k8s_version', 'versionLoad'], + [ + 'region', + 'node_pools', + 'label', + 'k8s_version', + 'versionLoad', + 'control_plane', + ], errors ); @@ -337,6 +345,7 @@ export const CreateCluster = () => { setIPv6Addr(newIpV6Addr); }} enableControlPlaneACL={controlPlaneACL} + errorText={errorMap.control_plane} ipV4Addr={ipV4Addr} ipV6Addr={ipV6Addr} setControlPlaneACL={setControlPlaneACL} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index ea6fd7a87ef..376ceb531b5 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -57,7 +57,8 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const _revisionID = data?.acl?.['revision-id']; - const _hideEnableFromUI = !clusterMigrated || !_enabled; + const enabledExists = _enabled !== undefined; + const shouldDefaultToEnabled = !clusterMigrated || !_enabled; // respective react states const [ipV4Addr, setIPv4Addr] = React.useState([]); @@ -78,13 +79,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { setRevisionID(_revisionID ? _revisionID : ''); setUpdateACLError(isErrorKubernetesACL?.[0].reason); setUpdating(false); - setSubmitButtonLabel( - _hideEnableFromUI - ? 'Enable IPACL' - : clusterMigrated - ? 'Update IPACL' - : 'Install IPACL' - ); + setSubmitButtonLabel(enabledExists ? 'Update IPACL' : 'Install IPACL'); refetchKubernetesACL(); } }, [open]); @@ -123,7 +118,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const payload: KubernetesControlPlaneACLPayload = { acl: { - enabled: _hideEnableFromUI ? true : controlPlaneACL, // both new cluster installations as well as all the states where the UI disabled the option for the user to enable, we default to true + enabled: enabledExists + ? controlPlaneACL + : shouldDefaultToEnabled || controlPlaneACL, // both new cluster installations as well as all the states where the UI disabled the option for the user to enable, we default to true 'revision-id': revisionID, ...((_ipv4.length > 0 || _ipv6.length > 0) && { addresses: { @@ -175,100 +172,77 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { [setIPv6Addr] ); - const ErrorMessage = () => { - if (!!updateError && clusterMigrated) { - return ( - - {updateError} - - ); - } - return <>; - }; + const ErrorMessage = !!updateError && clusterMigrated && ( + + {updateError} + + ); - const ClusterNeedsMigration = () => { - if (!clusterMigrated) { - return ( - - IPACL has not yet been installed on this cluster. During installation, - it may take up to 20 minutes before ACLs are fully enforced for the - first time. - - ); - } else { - return <>; - } - }; + const ClusterNeedsMigration = !clusterMigrated && ( + + IPACL has not yet been installed on this cluster. During installation, it + may take up to 20 minutes before ACLs are fully enforced for the first + time. + + ); - const EnabledCopy = () => { - return ( - - - Enabled - - - - A value of true results in a default policy of DENY. A value of - false results in a default policy of ALLOW (i.e., access controls - are disabled). When enabled, control plane access controls can only - be accessible through the defined IP CIDRs. - - + const EnabledCopy = ( + + + Enabled - ); - }; - - const RevisionID = () => { - if (clusterMigrated) { - return ( - <> - - setRevisionID(e.target.value)} - value={revisionID} - /> - - - ); - } else { - return <>; - } - }; + + + A value of true results in a default policy of DENY. A value of false + results in a default policy of ALLOW (i.e., access controls are + disabled). When enabled, control plane access controls can only be + accessible through the defined IP CIDRs. + + + + ); - const RevisionIDCopy = () => { - return ( - - - Revision ID - - - - Enables clients to track events related to ACL update requests and - enforcements. Optional field. If omitted, defaults to a randomly - generated string. - - + const RevisionIDCopy = ( + + + Revision ID - ); - }; + + + Enables clients to track events related to ACL update requests and + enforcements. Optional field. If omitted, defaults to a randomly + generated string. + + + + ); - const AddressesCopy = () => { - return ( - <> - - Addresses - - - - A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW - access to the control plane. - - - - ); - }; + const RevisionID = clusterMigrated && ( + <> + {RevisionIDCopy} + setRevisionID(e.target.value)} + value={revisionID} + /> + + + ); + + const AddressesCopy = ( + <> + + Addresses + + + + A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW access + to the control plane. + + + + ); return ( { - - {!_hideEnableFromUI && ( + {enabledExists && ( <> - + {EnabledCopy} { if (clusterMigrated) { - return setControlPlaneACL(e.target.checked); + setControlPlaneACL(e.target.checked); } }} checked={clusterMigrated ? controlPlaneACL : true} @@ -317,9 +290,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} - - - + {RevisionID} + {AddressesCopy} + {ErrorMessage} { @@ -364,7 +337,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { }} secondaryButtonProps={{ label: 'Cancel', onClick: closeDrawer }} /> - + {ClusterNeedsMigration} From 123590217f2c69a8c4dd38bbe5e632e3225ea765 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Mon, 7 Oct 2024 16:12:49 -0400 Subject: [PATCH 14/36] margin fixes --- .../KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx | 4 +++- .../KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx | 4 ++-- .../KubernetesClusterDetail/KubeSummaryPanel.styles.tsx | 4 ++++ .../Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx index 2d661f9585c..43befc3c3bb 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx @@ -122,7 +122,9 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { const availableActions = [ isLoadingKubernetesACL ? ( - + + + ) : failedMigrationStatus() ? ( ) : enabledACL ? ( diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 376ceb531b5..1ac90cf3356 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -271,7 +271,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { {enabledExists && ( <> {EnabledCopy} - + { label={'IPACL Enabled'} /> - + )} {RevisionID} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index 4858401adab..99ecdefe23c 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -64,6 +64,8 @@ export const StyledVPCBox = styled(Box, { label: 'StyledVPCBox' })( export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ alignItems: 'center', display: 'flex', + marginLeft: `-${theme.spacing(1)}`, + minHeight: theme.spacing(3), [theme.breakpoints.down('md')]: { alignItems: 'flex-start', flexDirection: 'column', @@ -72,6 +74,8 @@ export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ export const StyledLabelBox = styled(Box, { label: 'StyledLabelBox' })( ({ theme }) => ({ + alignItems: 'center', + display: 'flex', fontFamily: theme.font.bold, marginRight: theme.spacing(0.5), }) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index adcaad01a42..ff1684e9e44 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -246,7 +246,7 @@ export const KubeSummaryPanel = React.memo((props: Props) => { lg={8} xs={12} > - + Date: Mon, 7 Oct 2024 16:26:21 -0400 Subject: [PATCH 15/36] height issues --- .../KubernetesClusterDetail/KubeSummaryPanel.styles.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index 99ecdefe23c..e1e6db48dc8 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -64,12 +64,16 @@ export const StyledVPCBox = styled(Box, { label: 'StyledVPCBox' })( export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ alignItems: 'center', display: 'flex', - marginLeft: `-${theme.spacing(1)}`, - minHeight: theme.spacing(3), + [theme.breakpoints.down('lg')]: { + minHeight: theme.spacing(3), + }, [theme.breakpoints.down('md')]: { alignItems: 'flex-start', flexDirection: 'column', }, + [theme.breakpoints.up('lg')]: { + minHeight: theme.spacing(5), + }, })); export const StyledLabelBox = styled(Box, { label: 'StyledLabelBox' })( From 900e18f225d5a8b206105fea5a729b21344103f5 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 8 Oct 2024 15:40:09 -0400 Subject: [PATCH 16/36] quick initial cleanup --- packages/api-v4/src/kubernetes/kubernetes.ts | 1 - .../CreateCluster/ControlPlaneACLPane.tsx | 13 +- .../KubeClusterControlPlaneACL.tsx | 19 --- .../KubeSummaryPanel.styles.tsx | 152 ------------------ 4 files changed, 2 insertions(+), 183 deletions(-) diff --git a/packages/api-v4/src/kubernetes/kubernetes.ts b/packages/api-v4/src/kubernetes/kubernetes.ts index 50c5e38affd..402d8633a92 100644 --- a/packages/api-v4/src/kubernetes/kubernetes.ts +++ b/packages/api-v4/src/kubernetes/kubernetes.ts @@ -227,7 +227,6 @@ export const getKubernetesTypes = (params?: Params) => * getKubernetesClusterControlPlaneACL * * Return control plane access list about a single Kubernetes cluster - * */ export const getKubernetesClusterControlPlaneACL = (clusterID: number) => Request( diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index 992b6639493..ab0c0d2fe1e 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -23,15 +23,6 @@ export interface ControlPlaneACLProps { setControlPlaneACL: (enabled: boolean) => void; } -export const IPACLCopy = () => ( - - This is the text for Control Plane Access Control.{' '} - - Learn more. - - -); - export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { const { enableControlPlaneACL, @@ -43,7 +34,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { setControlPlaneACL, } = props; - const handleChange = (e: React.ChangeEvent) => { + const handleChange = () => { setControlPlaneACL(!enableControlPlaneACL); }; @@ -85,7 +76,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { handleChange(e)} + onChange={() => handleChange()} /> } label="Enable IPACL" diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx index 43befc3c3bb..746d54439d8 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx @@ -35,10 +35,6 @@ const useStyles = makeStyles()((theme: Theme) => ({ marginRight: theme.spacing(1), whiteSpace: 'nowrap', }, - iconTextOuter: { - flexBasis: '72%', - minWidth: 115, - }, item: { '&:first-of-type': { paddingTop: 0, @@ -49,21 +45,6 @@ const useStyles = makeStyles()((theme: Theme) => ({ paddingBottom: theme.spacing(1), paddingTop: theme.spacing(1), }, - mainGridContainer: { - position: 'relative', - [theme.breakpoints.up('lg')]: { - justifyContent: 'space-between', - }, - }, - root: { - marginBottom: theme.spacing(3), - padding: `${theme.spacing(2.5)} ${theme.spacing(2.5)} ${theme.spacing(3)}`, - }, - tooltip: { - '& .MuiTooltip-tooltip': { - minWidth: 320, - }, - }, })); export const KubeClusterControlPlaneACL = React.memo((props: Props) => { diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index e1e6db48dc8..e39be7c1c7f 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -1,66 +1,11 @@ // This component was built asuming an unmodified MUI
import { styled } from '@mui/material/styles'; -import Table from '@mui/material/Table'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Link } from 'react-router-dom'; import { Box } from 'src/components/Box'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; -import { TableCell } from 'src/components/TableCell'; -import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; import type { Theme } from '@mui/material/styles'; -// --------------------------------------------------------------------- -// Header Styles -// --------------------------------------------------------------------- - -export const StyledLink = styled(Link, { label: 'StyledLink' })( - ({ theme }) => ({ - '&:hover': { - color: theme.palette.primary.light, - textDecoration: 'underline', - }, - marginLeft: theme.spacing(), - }) -); - -// --------------------------------------------------------------------- -// Body Styles -// --------------------------------------------------------------------- - -export const StyledBodyGrid = styled(Grid, { label: 'StyledBodyGrid' })( - ({ theme }) => ({ - justifyContent: 'space-between', - padding: theme.spacing(2), - }) -); - -export const StyledColumnLabelGrid = styled(Grid, { - label: 'StyledColumnLabelGrid', -})(({ theme }) => ({ - color: theme.textColors.headlineStatic, - fontFamily: theme.font.bold, -})); - -export const StyledSummaryGrid = styled(Grid, { label: 'StyledSummaryGrid' })( - ({ theme }) => ({ - '& p': { - color: theme.textColors.tableStatic, - }, - }) -); - -export const StyledVPCBox = styled(Box, { label: 'StyledVPCBox' })( - ({ theme }) => ({ - padding: 0, - [theme.breakpoints.down('md')]: { - paddingBottom: theme.spacing(0.5), - }, - }) -); - export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ alignItems: 'center', display: 'flex', @@ -91,11 +36,6 @@ export const sxListItemMdBp = { padding: 0, }; -export const sxLastListItem = { - borderRight: 0, - paddingRight: 0, -}; - export const StyledListItem = styled(Typography, { label: 'StyledTypography' })( ({ theme }) => ({ color: theme.textColors.tableStatic, @@ -115,95 +55,3 @@ export const sxListItemFirstChild = (theme: Theme) => ({ }, }, }); - -// --------------------------------------------------------------------- -// AccessTable Styles -// --------------------------------------------------------------------- - -export const StyledTable = styled(Table, { label: 'StyledTable' })( - ({ theme }) => ({ - '& td': { - border: 'none', - borderBottom: `1px solid ${theme.bg.bgPaper}`, - fontSize: '0.875rem', - lineHeight: 1, - whiteSpace: 'nowrap', - }, - '& th': { - backgroundColor: - theme.name === 'light' ? theme.color.grey10 : theme.bg.app, - borderBottom: `1px solid ${theme.bg.bgPaper}`, - color: theme.textColors.textAccessTable, - fontFamily: theme.font.bold, - fontSize: '0.875rem', - lineHeight: 1, - padding: theme.spacing(), - textAlign: 'left', - whiteSpace: 'nowrap', - width: 170, - }, - '& tr': { - height: 32, - }, - border: 'none', - tableLayout: 'fixed', - }) -); - -export const StyledTableGrid = styled(Grid, { label: 'StyledTableGrid' })( - ({ theme }) => ({ - '&.MuiGrid-item': { - padding: 0, - paddingLeft: theme.spacing(), - }, - }) -); - -export const StyledTableCell = styled(TableCell, { label: 'StyledTableCell' })( - ({ theme }) => ({ - '& div': { - fontSize: 15, - }, - alignItems: 'center', - backgroundColor: theme.bg.bgAccessRow, - color: theme.textColors.tableStatic, - display: 'flex', - fontFamily: '"UbuntuMono", monospace, sans-serif', - justifyContent: 'space-between', - padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, - position: 'relative', - }) -); - -export const StyledCopyTooltip = styled(CopyTooltip, { - label: 'StyledCopyTooltip', -})({ - '& svg': { - height: `12px`, - opacity: 0, - width: `12px`, - }, -}); - -export const StyledGradientDiv = styled('div', { label: 'StyledGradientDiv' })( - ({ theme }) => ({ - '&:after': { - backgroundImage: `linear-gradient(to right, ${theme.bg.bgAccessRowTransparentGradient}, ${theme.bg.bgAccessRow});`, - bottom: 0, - content: '""', - height: '100%', - position: 'absolute', - right: 0, - width: 30, - }, - overflowX: 'auto', - overflowY: 'hidden', // For Edge - paddingRight: 15, - }) -); - -export const StyledTableRow = styled(TableRow, { label: 'StyledTableRow' })({ - '&:hover .copy-tooltip > svg, & .copy-tooltip:focus > svg': { - opacity: 1, - }, -}); From b324a6b9cb722886268f9ee97695daf894c3ff40 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 9 Oct 2024 15:35:57 -0400 Subject: [PATCH 17/36] refactor some styling, initial transition to react-hook-form? --- .../CreateCluster/CreateCluster.tsx | 28 +- .../KubeClusterControlPlaneACL.tsx | 116 ++---- .../KubeControlPaneACLDrawer.tsx | 331 ++++++++---------- .../KubeSummaryPanel.styles.tsx | 74 ++-- .../KubeSummaryPanel.tsx | 128 ++----- 5 files changed, 251 insertions(+), 426 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index c586adcabb1..e51739faaaf 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -367,21 +367,19 @@ export const CreateCluster = () => { {showControlPlaneACL && ( <> - - { - setIPv4Addr(newIpV4Addr); - }} - handleIPv6Change={(newIpV6Addr: ExtendedIP[]) => { - setIPv6Addr(newIpV6Addr); - }} - enableControlPlaneACL={controlPlaneACL} - errorText={errorMap.control_plane} - ipV4Addr={ipV4Addr} - ipV6Addr={ipV6Addr} - setControlPlaneACL={setControlPlaneACL} - /> - + { + setIPv4Addr(newIpV4Addr); + }} + handleIPv6Change={(newIpV6Addr: ExtendedIP[]) => { + setIPv6Addr(newIpV6Addr); + }} + enableControlPlaneACL={controlPlaneACL} + errorText={errorMap.control_plane} + ipV4Addr={ipV4Addr} + ipV6Addr={ipV6Addr} + setControlPlaneACL={setControlPlaneACL} + /> )} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx index 746d54439d8..e0416a26a98 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx @@ -1,15 +1,12 @@ -import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; import { Box } from 'src/components/Box'; +import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; import { CircleProgress } from 'src/components/CircleProgress'; -import { Typography } from 'src/components/Typography'; import { useKubernetesControlPlaneACLQuery } from 'src/queries/kubernetes'; import { pluralize } from 'src/utilities/pluralize'; import type { KubernetesCluster } from '@linode/api-v4'; -import type { Theme } from '@mui/material/styles'; interface Props { cluster: KubernetesCluster; @@ -17,39 +14,8 @@ interface Props { setControlPlaneACLMigrated: (s: boolean) => void; } -const useStyles = makeStyles()((theme: Theme) => ({ - aclElement: { - '&:hover': { - opacity: 0.7, - }, - '&:last-child': { - borderRight: 'none', - }, - alignItems: 'center', - borderRight: '1px solid #c4c4c4', - cursor: 'pointer', - display: 'flex', - }, - aclElementText: { - color: theme.textColors.linkActiveLight, - marginRight: theme.spacing(1), - whiteSpace: 'nowrap', - }, - item: { - '&:first-of-type': { - paddingTop: 0, - }, - '&:last-of-type': { - paddingBottom: 0, - }, - paddingBottom: theme.spacing(1), - paddingTop: theme.spacing(1), - }, -})); - export const KubeClusterControlPlaneACL = React.memo((props: Props) => { const { cluster, handleOpenDrawer, setControlPlaneACLMigrated } = props; - const { classes } = useStyles(); const { data: acl_response, @@ -57,74 +23,38 @@ export const KubeClusterControlPlaneACL = React.memo((props: Props) => { isLoading: isLoadingKubernetesACL, } = useKubernetesControlPlaneACLQuery(cluster.id); - const enabledACL = acl_response ? acl_response.acl.enabled : false; + const enabledACL = acl_response?.acl.enabled ?? false; // const revisionIDACL = acl_response ? acl_response.acl['revision-id'] : ''; - const totalIPv4 = acl_response?.acl.addresses?.ipv4?.length - ? acl_response?.acl.addresses?.ipv4?.length - : 0; - const totalIPv6 = acl_response?.acl.addresses?.ipv6?.length - ? acl_response?.acl.addresses?.ipv6?.length - : 0; + const totalIPv4 = acl_response?.acl.addresses?.ipv4?.length ?? 0; + const totalIPv6 = acl_response?.acl.addresses?.ipv6?.length ?? 0; const totalNumberIPs = totalIPv4 + totalIPv6; + // note to self: look into this method/if it's necessary const failedMigrationStatus = () => { // when a cluster has not migrated, the query will always fail setControlPlaneACLMigrated(!isErrorKubernetesACL); return isErrorKubernetesACL; }; - const EnabledCopy = () => { - return ( - - - {pluralize('IP Address', 'IP Addresses', totalNumberIPs)} - - - ); - }; - - const DisabledCopy = () => { - return ( - - Enable IPACL - - ); - }; - - const NotMigratedCopy = () => { - return ( - - - Install IPACL - - - ); - }; - - const availableActions = [ - isLoadingKubernetesACL ? ( - - - - ) : failedMigrationStatus() ? ( - - ) : enabledACL ? ( - - ) : ( - - ), - ]; + const determineButtonCopy = failedMigrationStatus() + ? 'Install IPACL' + : enabledACL + ? pluralize('IP Address', 'IP Addresses', totalNumberIPs) + : 'Enable IPACL'; return ( - - {availableActions} - + // will get rid of this fragment eventually...just doing some cleanup here and there + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + {isLoadingKubernetesACL ? ( + + + + ) : ( + + {determineButtonCopy} + + )} + ); }); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 1ac90cf3356..38ba5a33d52 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -1,7 +1,7 @@ import { Divider, Stack } from '@mui/material'; import { Box } from '@mui/material'; -import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; +import { useForm } from 'react-hook-form'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; @@ -30,59 +30,39 @@ interface Props { open: boolean; } +// wondering if I should get the data from a parent component instead? + export const KubeControlPlaneACLDrawer = (props: Props) => { const { closeDrawer, clusterId, clusterLabel, clusterMigrated, open } = props; - const [updateError, setUpdateACLError] = React.useState(); - const [updating, setUpdating] = React.useState(false); - const { data: data, error: isErrorKubernetesACL, isFetching: isFetchingKubernetesACL, isLoading: isLoadingKubernetesACL, - refetch: refetchKubernetesACL, + // refetch: refetchKubernetesACL, } = useKubernetesControlPlaneACLQuery(clusterId); - // dynamic variables mapped to JSON queried for this cluster - const _ipv4 = data?.acl?.addresses?.ipv4?.map((ip) => { + const ipv4 = data?.acl?.addresses?.ipv4?.map((ip) => { return stringToExtendedIP(ip); }); - - const _ipv6 = data?.acl?.addresses?.ipv6?.map((ip) => { + const ipv6 = data?.acl?.addresses?.ipv6?.map((ip) => { return stringToExtendedIP(ip); }); + const enabled = data?.acl?.enabled; + const revisionID = data?.acl?.['revision-id']; - const _enabled = data?.acl?.enabled; - - const _revisionID = data?.acl?.['revision-id']; - - const enabledExists = _enabled !== undefined; - const shouldDefaultToEnabled = !clusterMigrated || !_enabled; - - // respective react states - const [ipV4Addr, setIPv4Addr] = React.useState([]); - const [ipV6Addr, setIPv6Addr] = React.useState([]); - const [controlPlaneACL, setControlPlaneACL] = React.useState(false); - const [revisionID, setRevisionID] = React.useState(); - - const [submitButtonLabel, setSubmitButtonLabel] = React.useState(''); + const enabledExists = enabled !== undefined; + const shouldDefaultToEnabled = !clusterMigrated || !enabled; + // check if we really want this? // refetchOnMount isnt good enough for this query because // it is already mounted in the rendered Drawer - React.useEffect(() => { - if (open && !isLoadingKubernetesACL && !isFetchingKubernetesACL) { - // updates states based on queried data - setIPv4Addr(_ipv4 ? _ipv4 : [stringToExtendedIP('')]); - setIPv6Addr(_ipv6 ? _ipv6 : [stringToExtendedIP('')]); - setControlPlaneACL(_enabled ? _enabled : false); - setRevisionID(_revisionID ? _revisionID : ''); - setUpdateACLError(isErrorKubernetesACL?.[0].reason); - setUpdating(false); - setSubmitButtonLabel(enabledExists ? 'Update IPACL' : 'Install IPACL'); - refetchKubernetesACL(); - } - }, [open]); + // React.useEffect(() => { + // if (open && !isLoadingKubernetesACL && !isFetchingKubernetesACL) { + // refetchKubernetesACL(); // makes it fetch again + // } + // }, [open]); const { mutateAsync: updateKubernetesClusterControlPlaneACL, @@ -92,17 +72,39 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { clusterId ); - const updateCluster = () => { - setUpdateACLError(undefined); - setUpdating(true); + const { + formState: { errors, isSubmitting }, + handleSubmit, + reset, + setError, + setValue, + watch, + } = useForm({ + // need to make this eventually match the shape of KubernetesControlPlaneACLPayload hopefully + // defaultValues: { + // enabled: !!enabled, + // ipv4: ipv4 ?? [stringToExtendedIP('')], + // ipv6: ipv6 ?? [stringToExtendedIP('')], + // 'revision-id': revisionID, + // }, + values: { + enabled: !!enabled, + ipv4: ipv4 ?? [stringToExtendedIP('')], + ipv6: ipv6 ?? [stringToExtendedIP('')], + 'revision-id': revisionID, + }, + }); + + const values = watch(); - const _ipv4 = ipV4Addr + const updateCluster = handleSubmit(() => { + const _ipv4 = values.ipv4 .map((ip) => { return ip.address; }) .filter((ip) => ip != ''); - const _ipv6 = ipV6Addr + const _ipv6 = values.ipv6 .map((ip) => { return ip.address; }) @@ -119,9 +121,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const payload: KubernetesControlPlaneACLPayload = { acl: { enabled: enabledExists - ? controlPlaneACL - : shouldDefaultToEnabled || controlPlaneACL, // both new cluster installations as well as all the states where the UI disabled the option for the user to enable, we default to true - 'revision-id': revisionID, + ? values.enabled + : shouldDefaultToEnabled || values.enabled, // both new cluster installations as well as all the states where the UI disabled the option for the user to enable, we default to true + 'revision-id': values['revision-id'], ...((_ipv4.length > 0 || _ipv6.length > 0) && { addresses: { ...addressIPv4Payload, @@ -131,122 +133,34 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { }, }; - if (clusterMigrated) { - updateKubernetesClusterControlPlaneACL(payload) - .then(() => { - closeDrawer(); - setUpdating(false); - }) - .catch((err) => { - const regex = /(?<=\bControl\b: ).*/; - setUpdateACLError(err[0].reason.match(regex)); - setUpdating(false); - }); - } else { - updateKubernetesCluster({ - control_plane: payload, - }) - .then((_) => { - closeDrawer(); - setUpdating(false); - }) - .catch((err) => { - const regex = /(?<=\bControl\b: ).*/; - setUpdateACLError(err[0].reason.match(regex)); - setUpdating(false); + try { + if (clusterMigrated) { + updateKubernetesClusterControlPlaneACL(payload); + } else { + updateKubernetesCluster({ + control_plane: payload, }); + } + closeDrawer(); + } catch (errors) { + // .catch((err) => { + // const regex = /(?<=\bControl\b: ).*/; + // setUpdateACLError(err[0].reason.match(regex)); + // }); + for (const error of errors) { + if (error.field) { + setError(error.field, { message: error.reason }); + } else { + setError('root', { message: error.reason }); + } + } } - }; - - const handleIPv4ChangeCB = React.useCallback( - (_ips: ExtendedIP[]) => { - setIPv4Addr(_ips); - }, - [setIPv4Addr] - ); - - const handleIPv6ChangeCB = React.useCallback( - (_ips: ExtendedIP[]) => { - setIPv6Addr(_ips); - }, - [setIPv6Addr] - ); - - const ErrorMessage = !!updateError && clusterMigrated && ( - - {updateError} - - ); - - const ClusterNeedsMigration = !clusterMigrated && ( - - IPACL has not yet been installed on this cluster. During installation, it - may take up to 20 minutes before ACLs are fully enforced for the first - time. - - ); - - const EnabledCopy = ( - - - Enabled - - - - A value of true results in a default policy of DENY. A value of false - results in a default policy of ALLOW (i.e., access controls are - disabled). When enabled, control plane access controls can only be - accessible through the defined IP CIDRs. - - - - ); - - const RevisionIDCopy = ( - - - Revision ID - - - - Enables clients to track events related to ACL update requests and - enforcements. Optional field. If omitted, defaults to a randomly - generated string. - - - - ); - - const RevisionID = clusterMigrated && ( - <> - {RevisionIDCopy} - setRevisionID(e.target.value)} - value={revisionID} - /> - - - ); - - const AddressesCopy = ( - <> - - Addresses - - - - A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW access - to the control plane. - - - - ); + }); return ( reset()} open={open} title={'Control Plane Access Control (IPACL)'} wide @@ -258,29 +172,31 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { title={clusterLabel} > - - - - When a cluster is equipped with an ACL, the apiserver and - dashboard endpoints get mapped to a NodeBalancer address where - all traffic is protected through a Cloud Firewall. - - - + + When a cluster is equipped with an ACL, the apiserver and dashboard + endpoints get mapped to a NodeBalancer address where all traffic is + protected through a Cloud Firewall. + {enabledExists && ( <> - {EnabledCopy} + Enabled + + A value of true results in a default policy of DENY. A value of + false results in a default policy of ALLOW (i.e., access + controls are disabled). When enabled, control plane access + controls can only be accessible through the defined IP CIDRs. + { if (clusterMigrated) { - setControlPlaneACL(e.target.checked); + setValue('enabled', e.target.checked); } }} - checked={clusterMigrated ? controlPlaneACL : true} + checked={clusterMigrated ? values.enabled : true} name="ipacl-checkbox" /> } @@ -290,38 +206,67 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} - {RevisionID} - {AddressesCopy} - {ErrorMessage} + {clusterMigrated && ( + <> + Revision ID + + Enables clients to track events related to ACL update requests + and enforcements. Optional field. If omitted, defaults to a + randomly generated string. + + setValue('revision-id', e.target.value)} + value={values['revision-id']} + /> + + + )} + Addresses + + A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW + access to the control plane. + + {/* I am not sure if this matches - will need to check */} + {errors.root?.message && clusterMigrated && ( + + {errors.root.message} + + )} { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }); - handleIPv4ChangeCB(validatedIPs); - }} + onBlur={(ips: ExtendedIP[]) => + setValue( + 'ipv4', + validateIPs(ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }) + ) + } buttonText="Add IPv4 Address" - ips={ipV4Addr} + ips={values.ipv4} isLinkStyled - onChange={handleIPv4ChangeCB} + onChange={(ips: ExtendedIP[]) => setValue('ipv4', ips)} placeholder="0.0.0.0/0" title="IPv4 Addresses or CIDRs" /> { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv6 address.', - }); - handleIPv6ChangeCB(validatedIPs); - }} + onBlur={(ips: ExtendedIP[]) => + setValue( + 'ipv6', + validateIPs(ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }) + ) + } buttonText="Add IPv6 Address" - ips={ipV6Addr} + ips={values.ipv6} isLinkStyled - onChange={handleIPv6ChangeCB} + onChange={(ips: ExtendedIP[]) => setValue('ipv6', ips)} placeholder="::/0" title="IPv6 Addresses or CIDRs" /> @@ -330,14 +275,20 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { - {ClusterNeedsMigration} + {!clusterMigrated && ( + + IPACL has not yet been installed on this cluster. During + installation, it may take up to 20 minutes before ACLs are fully + enforced for the first time. + + )} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index e39be7c1c7f..8b5dc17e714 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -1,10 +1,8 @@ // This component was built asuming an unmodified MUI
import { styled } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import { Box } from 'src/components/Box'; -import { Typography } from 'src/components/Typography'; - -import type { Theme } from '@mui/material/styles'; export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ alignItems: 'center', @@ -12,12 +10,9 @@ export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ [theme.breakpoints.down('lg')]: { minHeight: theme.spacing(3), }, - [theme.breakpoints.down('md')]: { - alignItems: 'flex-start', - flexDirection: 'column', - }, [theme.breakpoints.up('lg')]: { minHeight: theme.spacing(5), + padding: `0px 10px`, }, })); @@ -30,28 +25,55 @@ export const StyledLabelBox = styled(Box, { label: 'StyledLabelBox' })( }) ); -export const sxListItemMdBp = { - borderRight: 0, - flex: '50%', - padding: 0, -}; +export const StyledActionRowGrid = styled(Grid, { + label: 'StyledActionRowGrid', +})({ + '& button': { + alignItems: 'flex-start', + }, + alignItems: 'flex-end', + alignSelf: 'stretch', + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + padding: '8px 0px', +}); -export const StyledListItem = styled(Typography, { label: 'StyledTypography' })( +export const StyledTagGrid = styled(Grid, { label: 'StyledTagGrid' })( ({ theme }) => ({ - color: theme.textColors.tableStatic, + // Tags Panel wrapper + '& > div:last-child': { + marginBottom: 0, + marginTop: 2, + width: '100%', + }, + '&.MuiGrid-item': { + paddingBottom: 0, + }, + alignItems: 'flex-end', + alignSelf: 'stretch', display: 'flex', - padding: `0px 10px`, - [theme.breakpoints.down('md')]: { - ...sxListItemMdBp, + flexDirection: 'column', + justifyContent: 'flex-end', + [theme.breakpoints.down('lg')]: { + width: '100%', + }, + [theme.breakpoints.up('lg')]: { + '& .MuiChip-root': { + marginLeft: 4, + marginRight: 0, + }, + // Add a Tag button + '& > div:first-of-type': { + justifyContent: 'flex-end', + marginTop: theme.spacing(4), + }, + // Tags Panel wrapper + '& > div:last-child': { + display: 'flex', + justifyContent: 'flex-end', + }, }, + width: '100%', }) ); - -export const sxListItemFirstChild = (theme: Theme) => ({ - [theme.breakpoints.down('md')]: { - ...sxListItemMdBp, - '&:first-of-type': { - paddingBottom: theme.spacing(0.5), - }, - }, -}); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index ff1684e9e44..6c6c10287dd 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -3,7 +3,6 @@ import { useTheme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Box } from 'src/components/Box'; @@ -30,77 +29,13 @@ import { KubeConfigDisplay } from './KubeConfigDisplay'; import { KubeConfigDrawer } from './KubeConfigDrawer'; import { KubeControlPlaneACLDrawer } from './KubeControlPaneACLDrawer'; import { + StyledActionRowGrid, StyledBox, StyledLabelBox, - StyledListItem, - sxListItemFirstChild, + StyledTagGrid, } from './KubeSummaryPanel.styles'; import type { KubernetesCluster } from '@linode/api-v4/lib/kubernetes'; -import type { Theme } from '@mui/material/styles'; - -const useStyles = makeStyles()((theme: Theme) => ({ - actionRow: { - '& button': { - alignItems: 'flex-start', - }, - alignItems: 'flex-end', - alignSelf: 'stretch', - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-end', - padding: '8px 0px', - }, - dashboard: { - '& svg': { - height: 14, - marginLeft: 4, - }, - alignItems: 'center', - display: 'flex', - }, - deleteClusterBtn: { - [theme.breakpoints.up('md')]: { - paddingRight: '8px', - }, - }, - tags: { - // Tags Panel wrapper - '& > div:last-child': { - marginBottom: 0, - marginTop: 2, - width: '100%', - }, - '&.MuiGrid-item': { - paddingBottom: 0, - }, - alignItems: 'flex-end', - alignSelf: 'stretch', - display: 'flex', - flexDirection: 'column', - justifyContent: 'flex-end', - [theme.breakpoints.down('lg')]: { - width: '100%', - }, - [theme.breakpoints.up('lg')]: { - '& .MuiChip-root': { - marginLeft: 4, - marginRight: 0, - }, - // Add a Tag button - '& > div:first-of-type': { - justifyContent: 'flex-end', - marginTop: theme.spacing(4), - }, - // Tags Panel wrapper - '& > div:last-child': { - display: 'flex', - justifyContent: 'flex-end', - }, - }, - width: '100%', - }, -})); interface Props { cluster: KubernetesCluster; @@ -109,7 +44,6 @@ interface Props { export const KubeSummaryPanel = React.memo((props: Props) => { const { cluster } = props; - const { classes } = useStyles(); const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); @@ -207,7 +141,7 @@ export const KubeSummaryPanel = React.memo((props: Props) => { lg={5} xs={12} > - + {cluster.control_plane.high_availability && ( { variant="outlined" /> )} - - + + { updateTags={handleUpdateTags} view="inline" /> - + } footer={ { xs={12} > - - IPACL: {' '} - - setControlPlaneACLDrawerOpen(true)} - setControlPlaneACLMigrated={setControlPlaneACLMigrated} - /> - - + IPACL: + setControlPlaneACLDrawerOpen(true)} + setControlPlaneACLMigrated={setControlPlaneACLMigrated} + /> } @@ -287,14 +200,25 @@ export const KubeSummaryPanel = React.memo((props: Props) => { onClick={() => { window.open(dashboard?.url, '_blank'); }} - className={classes.dashboard} + sx={{ + '& svg': { + height: '14px', + marginLeft: '4px', + }, + alignItems: 'center', + display: 'flex', + }} disabled={Boolean(dashboardError) || !dashboard} > Kubernetes Dashboard setIsDeleteDialogOpen(true)} > Delete Cluster From fcecc607be4be74b73d55e86ca3a4a49d61b089c Mon Sep 17 00:00:00 2001 From: Talmai Oliveira Date: Thu, 10 Oct 2024 00:58:25 -0400 Subject: [PATCH 18/36] adding some code description around the update cluster calls --- .../KubeControlPaneACLDrawer.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 38ba5a33d52..9612018b0fd 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -98,6 +98,28 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const values = watch(); const updateCluster = handleSubmit(() => { + // A quick note on the following code: + // + // - A non-IPACL'd cluster (denominated 'traditional') does not have IPACLs natively. + // The customer must then install IPACL (or 'migrate') on this cluster. + // This is done through a call to the updateKubernetesCluster endpoint. + // Only after a migration will the call to the updateKubernetesClusterControlPlaneACL + // endpoint be accepted. + // + // Do note that all new clusters automatically have IPACLs installed (even if the customer + // chooses to disable it during creation). + // + // For this reason, further in this code, we check whether the cluster has migrated or not + // before choosing which endpoint to use. + // + // - The address stanza of the JSON payload is optional. If provided though, that stanza must + // contain either/or/both IPv4 and IPv6. This is why there is additional code to properly + // check whether either exists, and only if they do, do we provide the addresses stanza + // to the payload + // + // - Hopefully this explains the behavior of this code, and why one must be very careful + // before introducing any clever/streamlined code - there's a reason to the mess :) + // const _ipv4 = values.ipv4 .map((ip) => { return ip.address; From 34da66293c2e7c4e30a7df7c6c03b7b06adba8b1 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 10 Oct 2024 11:56:53 -0400 Subject: [PATCH 19/36] move button file to Kube summary panel --- .../KubeClusterControlPlaneACL.tsx | 60 ------------------- .../KubeControlPaneACLDrawer.tsx | 1 + .../KubeSummaryPanel.tsx | 46 +++++++++++--- 3 files changed, 39 insertions(+), 68 deletions(-) delete mode 100644 packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx deleted file mode 100644 index e0416a26a98..00000000000 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; - -import { Box } from 'src/components/Box'; -import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; -import { CircleProgress } from 'src/components/CircleProgress'; -import { useKubernetesControlPlaneACLQuery } from 'src/queries/kubernetes'; -import { pluralize } from 'src/utilities/pluralize'; - -import type { KubernetesCluster } from '@linode/api-v4'; - -interface Props { - cluster: KubernetesCluster; - handleOpenDrawer: () => void; - setControlPlaneACLMigrated: (s: boolean) => void; -} - -export const KubeClusterControlPlaneACL = React.memo((props: Props) => { - const { cluster, handleOpenDrawer, setControlPlaneACLMigrated } = props; - - const { - data: acl_response, - isError: isErrorKubernetesACL, - isLoading: isLoadingKubernetesACL, - } = useKubernetesControlPlaneACLQuery(cluster.id); - - const enabledACL = acl_response?.acl.enabled ?? false; - // const revisionIDACL = acl_response ? acl_response.acl['revision-id'] : ''; - const totalIPv4 = acl_response?.acl.addresses?.ipv4?.length ?? 0; - const totalIPv6 = acl_response?.acl.addresses?.ipv6?.length ?? 0; - const totalNumberIPs = totalIPv4 + totalIPv6; - - // note to self: look into this method/if it's necessary - const failedMigrationStatus = () => { - // when a cluster has not migrated, the query will always fail - setControlPlaneACLMigrated(!isErrorKubernetesACL); - return isErrorKubernetesACL; - }; - - const determineButtonCopy = failedMigrationStatus() - ? 'Install IPACL' - : enabledACL - ? pluralize('IP Address', 'IP Addresses', totalNumberIPs) - : 'Enable IPACL'; - - return ( - // will get rid of this fragment eventually...just doing some cleanup here and there - // eslint-disable-next-line react/jsx-no-useless-fragment - <> - {isLoadingKubernetesACL ? ( - - - - ) : ( - - {determineButtonCopy} - - )} - - ); -}); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 9612018b0fd..fb0266c1617 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -23,6 +23,7 @@ import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; import type { ExtendedIP } from 'src/utilities/ipUtils'; interface Props { + // aclData?: KubernetesControlPlaneACLPayload; closeDrawer: () => void; clusterId: number; clusterLabel: string; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index 6c6c10287dd..2f5ef350fba 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -7,22 +7,25 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Box } from 'src/components/Box'; import { StyledActionButton } from 'src/components/Button/StyledActionButton'; +import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; import { Chip } from 'src/components/Chip'; +import { CircleProgress } from 'src/components/CircleProgress'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { EntityDetail } from 'src/components/EntityDetail/EntityDetail'; import { EntityHeader } from 'src/components/EntityHeader/EntityHeader'; import { Stack } from 'src/components/Stack'; import { TagCell } from 'src/components/TagCell/TagCell'; import { Typography } from 'src/components/Typography'; -import { KubeClusterControlPlaneACL } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterControlPlaneACL'; import { KubeClusterSpecs } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useKubernetesClusterMutation, + useKubernetesControlPlaneACLQuery, useKubernetesDashboardQuery, useResetKubeConfigMutation, } from 'src/queries/kubernetes'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; +import { pluralize } from 'src/utilities/pluralize'; import { DeleteKubernetesClusterDialog } from './DeleteKubernetesClusterDialog'; import { KubeConfigDisplay } from './KubeConfigDisplay'; @@ -53,10 +56,6 @@ export const KubeSummaryPanel = React.memo((props: Props) => { isControlPlaneACLDrawerOpen, setControlPlaneACLDrawerOpen, ] = React.useState(false); - const [ - isControlPlaneACLMigrated, - setControlPlaneACLMigrated, - ] = React.useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( @@ -80,6 +79,26 @@ export const KubeSummaryPanel = React.memo((props: Props) => { id: cluster.id, }); + const { + data: aclData, + error: isErrorKubernetesACL, + //isFetching: isFetchingKubernetesACL, + isLoading: isLoadingKubernetesACL, + // refetch: refetchKubernetesACL, + } = useKubernetesControlPlaneACLQuery(cluster.id); + + const enabledACL = aclData?.acl.enabled ?? false; + // const revisionIDACL = acl_response ? acl_response.acl['revision-id'] : ''; + const totalIPv4 = aclData?.acl.addresses?.ipv4?.length ?? 0; + const totalIPv6 = aclData?.acl.addresses?.ipv6?.length ?? 0; + const totalNumberIPs = totalIPv4 + totalIPv6; + + const determineIPACLButtonCopy = isErrorKubernetesACL + ? 'Install IPACL' + : enabledACL + ? pluralize('IP Address', 'IP Addresses', totalNumberIPs) + : 'Enable IPACL'; + const [ resetKubeConfigDialogOpen, setResetKubeConfigDialogOpen, @@ -176,11 +195,22 @@ export const KubeSummaryPanel = React.memo((props: Props) => { > IPACL: - + + + ) : ( + setControlPlaneACLDrawerOpen(true)} + > + {determineIPACLButtonCopy} + + )} + {/* setControlPlaneACLDrawerOpen(true)} setControlPlaneACLMigrated={setControlPlaneACLMigrated} - /> + /> */} } @@ -239,7 +269,7 @@ export const KubeSummaryPanel = React.memo((props: Props) => { closeDrawer={() => setControlPlaneACLDrawerOpen(false)} clusterId={cluster.id} clusterLabel={cluster.label} - clusterMigrated={isControlPlaneACLMigrated} + clusterMigrated={!isErrorKubernetesACL} open={isControlPlaneACLDrawerOpen} /> Date: Thu, 10 Oct 2024 13:56:15 -0400 Subject: [PATCH 20/36] invalidate query after installing ipacl --- .../KubeControlPaneACLDrawer.tsx | 16 ++++++++-------- .../KubernetesClusterDetail/KubeSummaryPanel.tsx | 7 +------ packages/manager/src/queries/kubernetes.ts | 15 +++++++++------ 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index fb0266c1617..e6a0d183f11 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -41,7 +41,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { error: isErrorKubernetesACL, isFetching: isFetchingKubernetesACL, isLoading: isLoadingKubernetesACL, - // refetch: refetchKubernetesACL, + refetch: refetchKubernetesACL, } = useKubernetesControlPlaneACLQuery(clusterId); const ipv4 = data?.acl?.addresses?.ipv4?.map((ip) => { @@ -51,7 +51,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { return stringToExtendedIP(ip); }); const enabled = data?.acl?.enabled; - const revisionID = data?.acl?.['revision-id']; + const revisionID = data?.acl?.['revision-id'] ?? ''; const enabledExists = enabled !== undefined; const shouldDefaultToEnabled = !clusterMigrated || !enabled; @@ -59,11 +59,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { // check if we really want this? // refetchOnMount isnt good enough for this query because // it is already mounted in the rendered Drawer - // React.useEffect(() => { - // if (open && !isLoadingKubernetesACL && !isFetchingKubernetesACL) { - // refetchKubernetesACL(); // makes it fetch again - // } - // }, [open]); + React.useEffect(() => { + if (open && !isLoadingKubernetesACL && !isFetchingKubernetesACL) { + refetchKubernetesACL(); // makes it fetch again + } + }, [open]); const { mutateAsync: updateKubernetesClusterControlPlaneACL, @@ -171,7 +171,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { // setUpdateACLError(err[0].reason.match(regex)); // }); for (const error of errors) { - if (error.field) { + if (error.field && error.field !== 'acl') { setError(error.field, { message: error.reason }); } else { setError('root', { message: error.reason }); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index 2f5ef350fba..4eb1b5127cc 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -82,7 +82,7 @@ export const KubeSummaryPanel = React.memo((props: Props) => { const { data: aclData, error: isErrorKubernetesACL, - //isFetching: isFetchingKubernetesACL, + // isFetching: isFetchingKubernetesACL, isLoading: isLoadingKubernetesACL, // refetch: refetchKubernetesACL, } = useKubernetesControlPlaneACLQuery(cluster.id); @@ -206,11 +206,6 @@ export const KubeSummaryPanel = React.memo((props: Props) => { {determineIPACLButtonCopy} )} - {/* setControlPlaneACLDrawerOpen(true)} - setControlPlaneACLMigrated={setControlPlaneACLMigrated} - /> */} } diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index 802bf2a6311..35983f34c6c 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -7,20 +7,20 @@ import { getKubeConfig, getKubernetesCluster, getKubernetesClusterBeta, + getKubernetesClusterControlPlaneACL, getKubernetesClusterDashboard, getKubernetesClusterEndpoints, getKubernetesClusters, getKubernetesTypes, getKubernetesVersions, - getKubernetesClusterControlPlaneACL, getNodePools, recycleAllNodes, recycleClusterNodes, recycleNode, resetKubeConfig, updateKubernetesCluster, - updateNodePool, updateKubernetesClusterControlPlaneACL, + updateNodePool, } from '@linode/api-v4'; import { createQueryKeys } from '@lukemorales/query-key-factory'; import { @@ -57,6 +57,10 @@ import type { export const kubernetesQueries = createQueryKeys('kubernetes', { cluster: (id: number) => ({ contextQueries: { + acl: { + queryFn: () => getKubernetesClusterControlPlaneACL(id), + queryKey: [id], + }, beta: { queryFn: () => getKubernetesClusterBeta(id), queryKey: [id], @@ -80,10 +84,6 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { queryFn: () => getAllNodePoolsForCluster(id), queryKey: null, }, - acl: { - queryFn: () => getKubernetesClusterControlPlaneACL(id), - queryKey: null, - }, }, queryFn: () => getKubernetesCluster(id), queryKey: [id], @@ -146,6 +146,9 @@ export const useKubernetesClusterMutation = (id: number) => { queryClient.invalidateQueries({ queryKey: kubernetesQueries.lists.queryKey, }); + queryClient.invalidateQueries({ + queryKey: kubernetesQueries.cluster(id)._ctx.acl.queryKey, + }); queryClient.setQueryData(kubernetesQueries.cluster(id).queryKey, data); }, } From ecbc30cce91183df61d82a906028e5753a51cd56 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 10 Oct 2024 16:21:47 -0400 Subject: [PATCH 21/36] additional cleanup --- .../CreateCluster/ControlPlaneACLIPInputs.tsx | 57 ++++++++++++ .../CreateCluster/ControlPlaneACLPane.tsx | 80 +++++----------- .../CreateCluster/CreateCluster.tsx | 5 + .../KubeControlPaneACLDrawer.tsx | 91 +++++++------------ 4 files changed, 122 insertions(+), 111 deletions(-) create mode 100644 packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLIPInputs.tsx diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLIPInputs.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLIPInputs.tsx new file mode 100644 index 00000000000..eb764ffafe9 --- /dev/null +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLIPInputs.tsx @@ -0,0 +1,57 @@ +import { Box } from '@mui/material'; +import * as React from 'react'; + +import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; + +import type { ExtendedIP } from 'src/utilities/ipUtils'; + +interface Props { + handleIPv4Blur: (ips: ExtendedIP[]) => void; + handleIPv4Change: (ips: ExtendedIP[]) => void; + handleIPv6Blur: (ips: ExtendedIP[]) => void; + handleIPv6Change: (ips: ExtendedIP[]) => void; + ipV4Addr: ExtendedIP[]; + ipV6Addr: ExtendedIP[]; + marginAfter?: boolean; +} + +export const ControlPlaneACLIPInputs = (props: Props) => { + const { + handleIPv4Blur, + handleIPv4Change, + handleIPv6Blur, + handleIPv6Change, + ipV4Addr, + ipV6Addr, + marginAfter, + } = props; + + const outerSx = marginAfter + ? { marginBottom: 3, maxWidth: 450 } + : { maxWidth: 450 }; + + return ( + + + + + + + ); +}; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index ab0c0d2fe1e..fb52ba56c51 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -1,16 +1,17 @@ -import { Box, FormLabel } from '@mui/material'; +import { FormLabel } from '@mui/material'; import * as React from 'react'; import { ErrorMessage } from 'src/components/ErrorMessage'; import { FormControl } from 'src/components/FormControl'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { Link } from 'src/components/Link'; -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 { ControlPlaneACLIPInputs } from './ControlPlaneACLIPInputs'; + import type { ExtendedIP } from 'src/utilities/ipUtils'; export interface ControlPlaneACLProps { @@ -34,24 +35,6 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { setControlPlaneACL, } = props; - const handleChange = () => { - setControlPlaneACL(!enableControlPlaneACL); - }; - - const handleIPv4ChangeCB = React.useCallback( - (_ips: ExtendedIP[]) => { - handleIPv4Change(_ips); - }, - [handleIPv4Change] - ); - - const handleIPv6ChangeCB = React.useCallback( - (_ips: ExtendedIP[]) => { - handleIPv6Change(_ips); - }, - [handleIPv6Change] - ); - return ( <> @@ -76,47 +59,34 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { handleChange()} + onChange={() => setControlPlaneACL(!enableControlPlaneACL)} /> } label="Enable IPACL" /> {enableControlPlaneACL && ( - - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }); - handleIPv4ChangeCB(validatedIPs); - }} - buttonText="Add IPv4 Address" - ips={ipV4Addr} - isLinkStyled - onChange={handleIPv4ChangeCB} - placeholder="0.0.0.0/0" - title="IPv4 Addresses or CIDRs" - /> - - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv6 address.', - }); - handleIPv6ChangeCB(validatedIPs); - }} - buttonText="Add IPv6 Address" - ips={ipV6Addr} - isLinkStyled - onChange={handleIPv6ChangeCB} - placeholder="::/0" - title="IPv6 Addresses or CIDRs" - /> - - + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }); + handleIPv4Change(validatedIPs); + }} + handleIPv6Blur={(_ips: ExtendedIP[]) => { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv6 address.', + }); + handleIPv6Change(validatedIPs); + }} + handleIPv4Change={handleIPv4Change} + handleIPv6Change={handleIPv4Change} + ipV4Addr={ipV4Addr} + ipV6Addr={ipV6Addr} + marginAfter + /> )} ); diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index e51739faaaf..913e7d2a8fb 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -141,6 +141,11 @@ export const CreateCluster = () => { }, [versionData]); const createCluster = () => { + if (ipV4Addr.some((ip) => ip.error) || ipV6Addr.some((ip) => ip.error)) { + scrollErrorIntoViewV2(formContainerRef); + return; + } + const { push } = history; setErrors(undefined); setSubmitting(true); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index e6a0d183f11..d696870f6d4 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -1,5 +1,5 @@ -import { Divider, Stack } from '@mui/material'; import { Box } from '@mui/material'; +import { Divider, Stack } from '@mui/material'; import * as React from 'react'; import { useForm } from 'react-hook-form'; @@ -7,7 +7,6 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; import { DrawerContent } from 'src/components/DrawerContent'; import { FormControlLabel } from 'src/components/FormControlLabel'; -import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle/Toggle'; @@ -19,11 +18,12 @@ import { } from 'src/queries/kubernetes'; import { stringToExtendedIP, validateIPs } from 'src/utilities/ipUtils'; +import { ControlPlaneACLIPInputs } from '../CreateCluster/ControlPlaneACLIPInputs'; + import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; import type { ExtendedIP } from 'src/utilities/ipUtils'; interface Props { - // aclData?: KubernetesControlPlaneACLPayload; closeDrawer: () => void; clusterId: number; clusterLabel: string; @@ -31,8 +31,6 @@ interface Props { open: boolean; } -// wondering if I should get the data from a parent component instead? - export const KubeControlPlaneACLDrawer = (props: Props) => { const { closeDrawer, clusterId, clusterLabel, clusterMigrated, open } = props; @@ -56,7 +54,6 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const enabledExists = enabled !== undefined; const shouldDefaultToEnabled = !clusterMigrated || !enabled; - // check if we really want this? // refetchOnMount isnt good enough for this query because // it is already mounted in the rendered Drawer React.useEffect(() => { @@ -81,13 +78,6 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { setValue, watch, } = useForm({ - // need to make this eventually match the shape of KubernetesControlPlaneACLPayload hopefully - // defaultValues: { - // enabled: !!enabled, - // ipv4: ipv4 ?? [stringToExtendedIP('')], - // ipv6: ipv6 ?? [stringToExtendedIP('')], - // 'revision-id': revisionID, - // }, values: { enabled: !!enabled, ipv4: ipv4 ?? [stringToExtendedIP('')], @@ -121,6 +111,13 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { // - Hopefully this explains the behavior of this code, and why one must be very careful // before introducing any clever/streamlined code - there's a reason to the mess :) // + if ( + values.ipv4.some((ip) => ip.error) || + values.ipv6.some((ip) => ip.error) + ) { + return; + } + const _ipv4 = values.ipv4 .map((ip) => { return ip.address; @@ -166,10 +163,6 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { } closeDrawer(); } catch (errors) { - // .catch((err) => { - // const regex = /(?<=\bControl\b: ).*/; - // setUpdateACLError(err[0].reason.match(regex)); - // }); for (const error of errors) { if (error.field && error.field !== 'acl') { setError(error.field, { message: error.reason }); @@ -239,6 +232,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { setValue('revision-id', e.target.value)} value={values['revision-id']} @@ -251,50 +245,35 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW access to the control plane. - {/* I am not sure if this matches - will need to check */} {errors.root?.message && clusterMigrated && ( {errors.root.message} )} - - - setValue( - 'ipv4', - validateIPs(ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }) - ) - } - buttonText="Add IPv4 Address" - ips={values.ipv4} - isLinkStyled - onChange={(ips: ExtendedIP[]) => setValue('ipv4', ips)} - placeholder="0.0.0.0/0" - title="IPv4 Addresses or CIDRs" - /> - - - setValue( - 'ipv6', - validateIPs(ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }) - ) - } - buttonText="Add IPv6 Address" - ips={values.ipv6} - isLinkStyled - onChange={(ips: ExtendedIP[]) => setValue('ipv6', ips)} - placeholder="::/0" - title="IPv6 Addresses or CIDRs" - /> - - + + setValue( + 'ipv4', + validateIPs(ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }) + ) + } + handleIPv6Blur={(ips: ExtendedIP[]) => + setValue( + 'ipv6', + validateIPs(ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }) + ) + } + handleIPv4Change={(ips: ExtendedIP[]) => setValue('ipv4', ips)} + handleIPv6Change={(ips: ExtendedIP[]) => setValue('ipv6', ips)} + ipV4Addr={values.ipv4} + ipV6Addr={values.ipv6} + /> Date: Fri, 11 Oct 2024 14:24:28 -0400 Subject: [PATCH 22/36] new UX changes, remove refresh --- .../KubeControlPaneACLDrawer.tsx | 282 +++++++++--------- .../KubeSummaryPanel.tsx | 10 +- 2 files changed, 146 insertions(+), 146 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index d696870f6d4..df823791cd5 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -23,6 +23,13 @@ import { ControlPlaneACLIPInputs } from '../CreateCluster/ControlPlaneACLIPInput import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; import type { ExtendedIP } from 'src/utilities/ipUtils'; +type IPACLDrawerFormState = { + enabled: boolean; + ipv4: ExtendedIP[]; + ipv6: ExtendedIP[]; + 'revision-id': string; +}; + interface Props { closeDrawer: () => void; clusterId: number; @@ -37,31 +44,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const { data: data, error: isErrorKubernetesACL, - isFetching: isFetchingKubernetesACL, isLoading: isLoadingKubernetesACL, - refetch: refetchKubernetesACL, } = useKubernetesControlPlaneACLQuery(clusterId); - const ipv4 = data?.acl?.addresses?.ipv4?.map((ip) => { - return stringToExtendedIP(ip); - }); - const ipv6 = data?.acl?.addresses?.ipv6?.map((ip) => { - return stringToExtendedIP(ip); - }); - const enabled = data?.acl?.enabled; - const revisionID = data?.acl?.['revision-id'] ?? ''; - - const enabledExists = enabled !== undefined; - const shouldDefaultToEnabled = !clusterMigrated || !enabled; - - // refetchOnMount isnt good enough for this query because - // it is already mounted in the rendered Drawer - React.useEffect(() => { - if (open && !isLoadingKubernetesACL && !isFetchingKubernetesACL) { - refetchKubernetesACL(); // makes it fetch again - } - }, [open]); - const { mutateAsync: updateKubernetesClusterControlPlaneACL, } = useKubernetesControlPlaneACLMutation(clusterId); @@ -70,25 +55,37 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { clusterId ); + const ipv4 = data?.acl?.addresses?.ipv4?.map((ip) => { + return stringToExtendedIP(ip); + }) ?? [stringToExtendedIP('')]; + const ipv6 = data?.acl?.addresses?.ipv6?.map((ip) => { + return stringToExtendedIP(ip); + }) ?? [stringToExtendedIP('')]; + + const initialValues: IPACLDrawerFormState = { + enabled: data?.acl?.enabled ?? false, + ipv4, + ipv6, + 'revision-id': data?.acl?.['revision-id'] ?? '', + }; + const { - formState: { errors, isSubmitting }, + formState: { errors, isDirty, isSubmitting }, handleSubmit, reset, setError, setValue, watch, - } = useForm({ + } = useForm({ + defaultValues: initialValues, values: { - enabled: !!enabled, - ipv4: ipv4 ?? [stringToExtendedIP('')], - ipv6: ipv6 ?? [stringToExtendedIP('')], - 'revision-id': revisionID, + ...initialValues, }, }); const values = watch(); - const updateCluster = handleSubmit(() => { + const updateCluster = () => { // A quick note on the following code: // // - A non-IPACL'd cluster (denominated 'traditional') does not have IPACLs natively. @@ -140,9 +137,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const payload: KubernetesControlPlaneACLPayload = { acl: { - enabled: enabledExists - ? values.enabled - : shouldDefaultToEnabled || values.enabled, // both new cluster installations as well as all the states where the UI disabled the option for the user to enable, we default to true + enabled: values.enabled, 'revision-id': values['revision-id'], ...((_ipv4.length > 0 || _ipv6.length > 0) && { addresses: { @@ -163,135 +158,140 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { } closeDrawer(); } catch (errors) { + const regex = /(?<=\bcontrol\b: ).*/; for (const error of errors) { - if (error.field && error.field !== 'acl') { - setError(error.field, { message: error.reason }); - } else { + if (error.reason.match(regex)) { setError('root', { message: error.reason }); } } } - }); + }; return ( reset()} open={open} - title={'Control Plane Access Control (IPACL)'} + title={'Control Plane Access Control List'} wide > - - - When a cluster is equipped with an ACL, the apiserver and dashboard - endpoints get mapped to a NodeBalancer address where all traffic is - protected through a Cloud Firewall. - - - {enabledExists && ( - <> - Enabled - - A value of true results in a default policy of DENY. A value of - false results in a default policy of ALLOW (i.e., access - controls are disabled). When enabled, control plane access - controls can only be accessible through the defined IP CIDRs. - - - { - if (clusterMigrated) { - setValue('enabled', e.target.checked); - } - }} - checked={clusterMigrated ? values.enabled : true} - name="ipacl-checkbox" - /> +
+ + + When a cluster is equipped with an ACL, the apiserver and + dashboard endpoints get mapped to a NodeBalancer address where all + traffic is protected through a Cloud Firewall. + + + Enabled + + A value of true results in a default policy of DENY. A value of + false results in a default policy of ALLOW (i.e., access controls + are disabled). When enabled, control plane access controls can + only be accessible through the defined IP CIDRs. + + + { + setValue('enabled', e.target.checked, { + shouldDirty: true, + }); + }} + checked={values.enabled ?? false} + name="ipacl-checkbox" + /> + } + label={'IPACL Enabled'} + /> + + + {clusterMigrated && ( + <> + Revision ID + + Enables clients to track events related to ACL update requests + and enforcements. Optional field. If omitted, defaults to a + randomly generated string. + + + setValue('revision-id', e.target.value, { + shouldDirty: true, + }) } - label={'IPACL Enabled'} + data-qa-label-input + errorText={errors['revision-id']?.message} + label="Revision ID" + value={values['revision-id']} /> -
- - - )} - {clusterMigrated && ( - <> - Revision ID - - Enables clients to track events related to ACL update requests - and enforcements. Optional field. If omitted, defaults to a - randomly generated string. - - setValue('revision-id', e.target.value)} - value={values['revision-id']} - /> - - - )} - Addresses - - A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW - access to the control plane. - - {errors.root?.message && clusterMigrated && ( - - {errors.root.message} - - )} - - setValue( - 'ipv4', - validateIPs(ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }) - ) - } - handleIPv6Blur={(ips: ExtendedIP[]) => - setValue( - 'ipv6', - validateIPs(ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }) - ) - } - handleIPv4Change={(ips: ExtendedIP[]) => setValue('ipv4', ips)} - handleIPv6Change={(ips: ExtendedIP[]) => setValue('ipv6', ips)} - ipV4Addr={values.ipv4} - ipV6Addr={values.ipv6} - /> - - {!clusterMigrated && ( - - IPACL has not yet been installed on this cluster. During - installation, it may take up to 20 minutes before ACLs are fully - enforced for the first time. - - )} -
+ + + )} + Addresses + + A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW + access to the control plane. + + {errors.root?.message && clusterMigrated && ( + + {errors.root.message} + + )} + + setValue( + 'ipv4', + validateIPs(ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }) + ) + } + handleIPv4Change={(ips: ExtendedIP[]) => + setValue('ipv4', ips, { shouldDirty: true }) + } + handleIPv6Blur={(ips: ExtendedIP[]) => + setValue( + 'ipv6', + validateIPs(ips, { + allowEmptyAddress: false, + errorMessage: 'Must be a valid IPv4 address.', + }) + ) + } + handleIPv6Change={(ips: ExtendedIP[]) => + setValue('ipv6', ips, { shouldDirty: true }) + } + ipV4Addr={values.ipv4} + ipV6Addr={values.ipv6} + /> + + {!clusterMigrated && ( + + IPACL has not yet been installed on this cluster. During + installation, it may take up to 20 minutes before ACLs are fully + enforced for the first time. + + )} + +
); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index 4eb1b5127cc..44aab106bde 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -93,11 +93,9 @@ export const KubeSummaryPanel = React.memo((props: Props) => { const totalIPv6 = aclData?.acl.addresses?.ipv6?.length ?? 0; const totalNumberIPs = totalIPv4 + totalIPv6; - const determineIPACLButtonCopy = isErrorKubernetesACL - ? 'Install IPACL' - : enabledACL + const determineIPACLButtonCopy = enabledACL ? pluralize('IP Address', 'IP Addresses', totalNumberIPs) - : 'Enable IPACL'; + : 'Enable'; const [ resetKubeConfigDialogOpen, @@ -194,7 +192,9 @@ export const KubeSummaryPanel = React.memo((props: Props) => { xs={12} > - IPACL: + + Control Plane ACL: + {isLoadingKubernetesACL ? ( From 6183ec00a2fcaf34ebc332ae743c9b8aea512a9e Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Fri, 11 Oct 2024 14:42:44 -0400 Subject: [PATCH 23/36] make function async --- .../KubeControlPaneACLDrawer.tsx | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index df823791cd5..22ba14e72df 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -24,6 +24,10 @@ import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; import type { ExtendedIP } from 'src/utilities/ipUtils'; type IPACLDrawerFormState = { + acl: IPACLDrawerACLState; +}; + +type IPACLDrawerACLState = { enabled: boolean; ipv4: ExtendedIP[]; ipv6: ExtendedIP[]; @@ -63,10 +67,12 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { }) ?? [stringToExtendedIP('')]; const initialValues: IPACLDrawerFormState = { - enabled: data?.acl?.enabled ?? false, - ipv4, - ipv6, - 'revision-id': data?.acl?.['revision-id'] ?? '', + acl: { + enabled: data?.acl?.enabled ?? false, + ipv4, + ipv6, + 'revision-id': data?.acl?.['revision-id'] ?? '', + }, }; const { @@ -84,8 +90,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { }); const values = watch(); + const { acl } = values; - const updateCluster = () => { + const updateCluster = async () => { // A quick note on the following code: // // - A non-IPACL'd cluster (denominated 'traditional') does not have IPACLs natively. @@ -108,20 +115,17 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { // - Hopefully this explains the behavior of this code, and why one must be very careful // before introducing any clever/streamlined code - there's a reason to the mess :) // - if ( - values.ipv4.some((ip) => ip.error) || - values.ipv6.some((ip) => ip.error) - ) { + if (acl.ipv4.some((ip) => ip.error) || acl.ipv6.some((ip) => ip.error)) { return; } - const _ipv4 = values.ipv4 + const _ipv4 = acl.ipv4 .map((ip) => { return ip.address; }) .filter((ip) => ip != ''); - const _ipv6 = values.ipv6 + const _ipv6 = acl.ipv6 .map((ip) => { return ip.address; }) @@ -137,8 +141,8 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { const payload: KubernetesControlPlaneACLPayload = { acl: { - enabled: values.enabled, - 'revision-id': values['revision-id'], + enabled: acl.enabled, + 'revision-id': acl['revision-id'], ...((_ipv4.length > 0 || _ipv6.length > 0) && { addresses: { ...addressIPv4Payload, @@ -150,18 +154,19 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { try { if (clusterMigrated) { - updateKubernetesClusterControlPlaneACL(payload); + await updateKubernetesClusterControlPlaneACL(payload); } else { - updateKubernetesCluster({ + await updateKubernetesCluster({ control_plane: payload, }); } closeDrawer(); } catch (errors) { - const regex = /(?<=\bcontrol\b: ).*/; for (const error of errors) { - if (error.reason.match(regex)) { - setError('root', { message: error.reason }); + if (error.field) { + setError(error.field, { message: error.message }); + } else { + setError('root', { message: error.message }); } } } @@ -182,7 +187,12 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { title={clusterLabel} >
- + {errors.root?.message && ( + + {errors.root.message} + + )} + When a cluster is equipped with an ACL, the apiserver and dashboard endpoints get mapped to a NodeBalancer address where all @@ -201,11 +211,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { control={ { - setValue('enabled', e.target.checked, { + setValue('acl.enabled', e.target.checked, { shouldDirty: true, }); }} - checked={values.enabled ?? false} + checked={acl.enabled ?? false} name="ipacl-checkbox" /> } @@ -223,14 +233,13 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { - setValue('revision-id', e.target.value, { + setValue('acl.revision-id', e.target.value, { shouldDirty: true, }) } data-qa-label-input - errorText={errors['revision-id']?.message} label="Revision ID" - value={values['revision-id']} + value={acl['revision-id']} /> @@ -240,15 +249,15 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW access to the control plane. - {errors.root?.message && clusterMigrated && ( + {errors.acl?.message && clusterMigrated && ( - {errors.root.message} + {errors.acl.message} )} setValue( - 'ipv4', + 'acl.ipv4', validateIPs(ips, { allowEmptyAddress: false, errorMessage: 'Must be a valid IPv4 address.', @@ -256,11 +265,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { ) } handleIPv4Change={(ips: ExtendedIP[]) => - setValue('ipv4', ips, { shouldDirty: true }) + setValue('acl.ipv4', ips, { shouldDirty: true }) } handleIPv6Blur={(ips: ExtendedIP[]) => setValue( - 'ipv6', + 'acl.ipv6', validateIPs(ips, { allowEmptyAddress: false, errorMessage: 'Must be a valid IPv4 address.', @@ -268,10 +277,10 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { ) } handleIPv6Change={(ips: ExtendedIP[]) => - setValue('ipv6', ips, { shouldDirty: true }) + setValue('acl.ipv6', ips, { shouldDirty: true }) } - ipV4Addr={values.ipv4} - ipV6Addr={values.ipv6} + ipV4Addr={acl.ipv4} + ipV6Addr={acl.ipv6} /> Date: Fri, 11 Oct 2024 15:19:38 -0400 Subject: [PATCH 24/36] some bit more cleanup --- .../KubeSummaryPanel.styles.tsx | 9 --------- .../KubernetesClusterDetail/KubeSummaryPanel.tsx | 13 +++++++------ 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index 8b5dc17e714..bee54c5e09f 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -16,15 +16,6 @@ export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ }, })); -export const StyledLabelBox = styled(Box, { label: 'StyledLabelBox' })( - ({ theme }) => ({ - alignItems: 'center', - display: 'flex', - fontFamily: theme.font.bold, - marginRight: theme.spacing(0.5), - }) -); - export const StyledActionRowGrid = styled(Grid, { label: 'StyledActionRowGrid', })({ diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index 44aab106bde..ac38a5cd731 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -34,7 +34,6 @@ import { KubeControlPlaneACLDrawer } from './KubeControlPaneACLDrawer'; import { StyledActionRowGrid, StyledBox, - StyledLabelBox, StyledTagGrid, } from './KubeSummaryPanel.styles'; @@ -82,13 +81,10 @@ export const KubeSummaryPanel = React.memo((props: Props) => { const { data: aclData, error: isErrorKubernetesACL, - // isFetching: isFetchingKubernetesACL, isLoading: isLoadingKubernetesACL, - // refetch: refetchKubernetesACL, } = useKubernetesControlPlaneACLQuery(cluster.id); const enabledACL = aclData?.acl.enabled ?? false; - // const revisionIDACL = acl_response ? acl_response.acl['revision-id'] : ''; const totalIPv4 = aclData?.acl.addresses?.ipv4?.length ?? 0; const totalIPv6 = aclData?.acl.addresses?.ipv6?.length ?? 0; const totalNumberIPs = totalIPv4 + totalIPv6; @@ -192,9 +188,14 @@ export const KubeSummaryPanel = React.memo((props: Props) => { xs={12} > - + Control Plane ACL: - + {isLoadingKubernetesACL ? ( From a67a414bed6a7ab7e3dec972a6fba0f883a59b97 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 15 Oct 2024 11:19:44 -0400 Subject: [PATCH 25/36] use shape of payload?? need to cleanup --- .../ControlPlaneACLIPInputsV2.tsx | 82 ++++++++++++ .../KubeControlPaneACLDrawer.tsx | 119 ++++++------------ packages/validation/src/kubernetes.schema.ts | 44 +++++++ 3 files changed, 164 insertions(+), 81 deletions(-) create mode 100644 packages/manager/src/features/Kubernetes/KubernetesClusterDetail/ControlPlaneACLIPInputsV2.tsx diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/ControlPlaneACLIPInputsV2.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/ControlPlaneACLIPInputsV2.tsx new file mode 100644 index 00000000000..f5da4384c9f --- /dev/null +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/ControlPlaneACLIPInputsV2.tsx @@ -0,0 +1,82 @@ +import { Box } from '@mui/material'; +import * as React from 'react'; + +import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; + +import type { FieldError, Merge } from 'react-hook-form'; +import type { ExtendedIP } from 'src/utilities/ipUtils'; + +interface Props { + handleIPv4Change: (ips: string[]) => void; + handleIPv6Change: (ips: string[]) => void; + ipV4Addr: string[]; + ipV6Addr: string[]; + ipv4Errors?: Merge; + ipv6Errors?: Merge; + marginAfter?: boolean; +} + +export const ControlPlaneACLIPInputsV2 = (props: Props) => { + const { + handleIPv4Change, + handleIPv6Change, + ipV4Addr, + ipV6Addr, + ipv4Errors, + ipv6Errors, + marginAfter, + } = props; + + const extendedIPv4: ExtendedIP[] = + ipV4Addr?.map((ip, idx) => { + return { + address: ip, + error: ipv4Errors ? ipv4Errors[idx]?.message : '', + }; + }) ?? []; + + const extendedIPv6: ExtendedIP[] = + ipV6Addr?.map((ip, idx) => { + return { + address: ip, + error: ipv6Errors ? ipv6Errors[idx]?.message : '', + }; + }) ?? []; + + const outerSx = marginAfter + ? { marginBottom: 3, maxWidth: 450 } + : { maxWidth: 450 }; + + return ( + + { + const _ips = ips.map((ip) => { + return ip.address; + }); + handleIPv4Change(_ips); + }} + buttonText="Add IPv4 Address" + ips={extendedIPv4} + isLinkStyled + placeholder="0.0.0.0/0" + title="IPv4 Addresses or CIDRs" + /> + + { + const _ips = ips.map((ip) => { + return ip.address; + }); + handleIPv6Change(_ips); + }} + buttonText="Add IPv6 Address" + ips={extendedIPv6} + isLinkStyled + placeholder="::/0" + title="IPv6 Addresses or CIDRs" + /> + + + ); +}; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 22ba14e72df..adf24cc15a3 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -1,3 +1,5 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { kubernetesControlPlaneACLPayloadSchema } from '@linode/validation'; import { Box } from '@mui/material'; import { Divider, Stack } from '@mui/material'; import * as React from 'react'; @@ -16,23 +18,10 @@ import { useKubernetesControlPlaneACLMutation, useKubernetesControlPlaneACLQuery, } from 'src/queries/kubernetes'; -import { stringToExtendedIP, validateIPs } from 'src/utilities/ipUtils'; -import { ControlPlaneACLIPInputs } from '../CreateCluster/ControlPlaneACLIPInputs'; +import { ControlPlaneACLIPInputsV2 } from './ControlPlaneACLIPInputsV2'; import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; -import type { ExtendedIP } from 'src/utilities/ipUtils'; - -type IPACLDrawerFormState = { - acl: IPACLDrawerACLState; -}; - -type IPACLDrawerACLState = { - enabled: boolean; - ipv4: ExtendedIP[]; - ipv6: ExtendedIP[]; - 'revision-id': string; -}; interface Props { closeDrawer: () => void; @@ -59,22 +48,6 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { clusterId ); - const ipv4 = data?.acl?.addresses?.ipv4?.map((ip) => { - return stringToExtendedIP(ip); - }) ?? [stringToExtendedIP('')]; - const ipv6 = data?.acl?.addresses?.ipv6?.map((ip) => { - return stringToExtendedIP(ip); - }) ?? [stringToExtendedIP('')]; - - const initialValues: IPACLDrawerFormState = { - acl: { - enabled: data?.acl?.enabled ?? false, - ipv4, - ipv6, - 'revision-id': data?.acl?.['revision-id'] ?? '', - }, - }; - const { formState: { errors, isDirty, isSubmitting }, handleSubmit, @@ -82,10 +55,19 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { setError, setValue, watch, - } = useForm({ - defaultValues: initialValues, + } = useForm({ + defaultValues: data, + mode: 'onBlur', + resolver: yupResolver(kubernetesControlPlaneACLPayloadSchema), values: { - ...initialValues, + acl: { + addresses: { + ipv4: data?.acl?.addresses?.ipv4 ?? [''], + ipv6: data?.acl?.addresses?.ipv6 ?? [''], + }, + enabled: data?.acl?.enabled ?? false, + 'revision-id': data?.acl?.['revision-id'] ?? '', + }, }, }); @@ -115,29 +97,17 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { // - Hopefully this explains the behavior of this code, and why one must be very careful // before introducing any clever/streamlined code - there's a reason to the mess :) // - if (acl.ipv4.some((ip) => ip.error) || acl.ipv6.some((ip) => ip.error)) { - return; - } + // if (acl.ipv4.some((ip) => ip.error) || acl.ipv6.some((ip) => ip.error)) { + // return; + // } - const _ipv4 = acl.ipv4 - .map((ip) => { - return ip.address; - }) - .filter((ip) => ip != ''); + const _ipv4 = acl.addresses?.ipv4 + ? acl.addresses.ipv4.filter((ip) => ip !== '') + : []; - const _ipv6 = acl.ipv6 - .map((ip) => { - return ip.address; - }) - .filter((ip) => ip != ''); - - const addressIPv4Payload = { - ...(_ipv4.length > 0 && { ipv4: _ipv4 }), - }; - - const addressIPv6Payload = { - ...(_ipv6.length > 0 && { ipv6: _ipv6 }), - }; + const _ipv6 = acl.addresses?.ipv6 + ? acl.addresses.ipv6.filter((ip) => ip !== '') + : []; const payload: KubernetesControlPlaneACLPayload = { acl: { @@ -145,8 +115,8 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { 'revision-id': acl['revision-id'], ...((_ipv4.length > 0 || _ipv6.length > 0) && { addresses: { - ...addressIPv4Payload, - ...addressIPv6Payload, + ...(_ipv4.length > 0 && { ipv4: _ipv4 }), + ...(_ipv6.length > 0 && { ipv6: _ipv6 }), }, }), }, @@ -172,6 +142,8 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { } }; + // console.log('the errors', values, errors); + return ( { }) } data-qa-label-input + errorText={errors.acl?.['revision-id']?.message} label="Revision ID" value={acl['revision-id']} /> @@ -254,33 +227,17 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { {errors.acl.message} )} - - setValue( - 'acl.ipv4', - validateIPs(ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }) - ) - } - handleIPv4Change={(ips: ExtendedIP[]) => - setValue('acl.ipv4', ips, { shouldDirty: true }) - } - handleIPv6Blur={(ips: ExtendedIP[]) => - setValue( - 'acl.ipv6', - validateIPs(ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }) - ) + + setValue('acl.addresses.ipv4', ips, { shouldDirty: true }) } - handleIPv6Change={(ips: ExtendedIP[]) => - setValue('acl.ipv6', ips, { shouldDirty: true }) + handleIPv6Change={(ips: string[]) => + setValue('acl.addresses.ipv6', ips, { shouldDirty: true }) } - ipV4Addr={acl.ipv4} - ipV6Addr={acl.ipv6} + ipV4Addr={acl.addresses?.ipv4 ?? []} + ipV6Addr={acl.addresses?.ipv6 ?? []} + ipv4Errors={errors.acl?.addresses?.ipv4} + ipv6Errors={errors.acl?.addresses?.ipv6} /> { + if (value !== '' && value === undefined) { + return false; + } + + return ( + value === '' || + ipaddr.IPv4.isValid(value) || + ipaddr.IPv4.isValidCIDR(value) + ); + }, +}); + +const ipv6Validation = string().test({ + message: 'Must be a valid IPv6 address.', + test: (value) => { + if (value !== '' && value === undefined) { + return false; + } + + return ( + value === '' || + ipaddr.IPv6.isValid(value) || + ipaddr.IPv6.isValidCIDR(value) + ); + }, +}); + +const controlPlaneACLOptionsSchema = object().shape({ + enabled: boolean(), + 'revision-id': string(), + addresses: object().shape({ + ipv4: array().of(ipv4Validation), + ipv6: array().of(ipv6Validation), + }), +}); + +export const kubernetesControlPlaneACLPayloadSchema = object().shape({ + acl: controlPlaneACLOptionsSchema, +}); From 10257fb967f506571b7cc881b0ee2aec062df33a Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 15 Oct 2024 13:58:30 -0400 Subject: [PATCH 26/36] make more generic --- .../MultipleIPInput/MultipleIPInput.tsx | 4 +- .../MultipleNonExtendedIPInput.tsx | 46 +++++++++++ .../CreateCluster/CreateCluster.tsx | 4 +- .../ControlPlaneACLIPInputsV2.tsx | 82 ------------------- .../KubeControlPaneACLDrawer.tsx | 45 ++++++---- 5 files changed, 79 insertions(+), 102 deletions(-) create mode 100644 packages/manager/src/components/MultipleIPInput/MultipleNonExtendedIPInput.tsx delete mode 100644 packages/manager/src/features/Kubernetes/KubernetesClusterDetail/ControlPlaneACLIPInputsV2.tsx diff --git a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx index 00d0022ea4a..a882a33c48a 100644 --- a/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx +++ b/packages/manager/src/components/MultipleIPInput/MultipleIPInput.tsx @@ -58,7 +58,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, })); -interface Props { +export interface MultipeIPInputProps { buttonText?: string; className?: string; error?: string; @@ -76,7 +76,7 @@ interface Props { tooltip?: string; } -export const MultipleIPInput = React.memo((props: Props) => { +export const MultipleIPInput = React.memo((props: MultipeIPInputProps) => { const { buttonText, className, diff --git a/packages/manager/src/components/MultipleIPInput/MultipleNonExtendedIPInput.tsx b/packages/manager/src/components/MultipleIPInput/MultipleNonExtendedIPInput.tsx new file mode 100644 index 00000000000..3a709de3c0c --- /dev/null +++ b/packages/manager/src/components/MultipleIPInput/MultipleNonExtendedIPInput.tsx @@ -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 { + ipErrors?: Merge; + 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 ( + { + const _ips = ips.map((ip) => { + return ip.address; + }); + onNonExtendedIPChange(_ips); + }} + ips={extendedIPs} + /> + ); +}; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 913e7d2a8fb..07e1ae89f76 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -158,13 +158,13 @@ export const CreateCluster = () => { .map((ip) => { return ip.address; }) - .filter((ip) => ip != ''); + .filter((ip) => ip !== ''); const _ipv6 = ipV6Addr .map((ip) => { return ip.address; }) - .filter((ip) => ip != ''); + .filter((ip) => ip !== ''); const addressIPv4Payload = { ...(_ipv4.length > 0 && { ipv4: _ipv4 }), diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/ControlPlaneACLIPInputsV2.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/ControlPlaneACLIPInputsV2.tsx deleted file mode 100644 index f5da4384c9f..00000000000 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/ControlPlaneACLIPInputsV2.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Box } from '@mui/material'; -import * as React from 'react'; - -import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; - -import type { FieldError, Merge } from 'react-hook-form'; -import type { ExtendedIP } from 'src/utilities/ipUtils'; - -interface Props { - handleIPv4Change: (ips: string[]) => void; - handleIPv6Change: (ips: string[]) => void; - ipV4Addr: string[]; - ipV6Addr: string[]; - ipv4Errors?: Merge; - ipv6Errors?: Merge; - marginAfter?: boolean; -} - -export const ControlPlaneACLIPInputsV2 = (props: Props) => { - const { - handleIPv4Change, - handleIPv6Change, - ipV4Addr, - ipV6Addr, - ipv4Errors, - ipv6Errors, - marginAfter, - } = props; - - const extendedIPv4: ExtendedIP[] = - ipV4Addr?.map((ip, idx) => { - return { - address: ip, - error: ipv4Errors ? ipv4Errors[idx]?.message : '', - }; - }) ?? []; - - const extendedIPv6: ExtendedIP[] = - ipV6Addr?.map((ip, idx) => { - return { - address: ip, - error: ipv6Errors ? ipv6Errors[idx]?.message : '', - }; - }) ?? []; - - const outerSx = marginAfter - ? { marginBottom: 3, maxWidth: 450 } - : { maxWidth: 450 }; - - return ( - - { - const _ips = ips.map((ip) => { - return ip.address; - }); - handleIPv4Change(_ips); - }} - buttonText="Add IPv4 Address" - ips={extendedIPv4} - isLinkStyled - placeholder="0.0.0.0/0" - title="IPv4 Addresses or CIDRs" - /> - - { - const _ips = ips.map((ip) => { - return ip.address; - }); - handleIPv6Change(_ips); - }} - buttonText="Add IPv6 Address" - ips={extendedIPv6} - isLinkStyled - placeholder="::/0" - title="IPv6 Addresses or CIDRs" - /> - - - ); -}; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index adf24cc15a3..1757ad4a5bc 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -9,6 +9,7 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; import { DrawerContent } from 'src/components/DrawerContent'; import { FormControlLabel } from 'src/components/FormControlLabel'; +import { MultipleNonExtendedIPInput } from 'src/components/MultipleIPInput/MultipleNonExtendedIPInput'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle/Toggle'; @@ -19,8 +20,6 @@ import { useKubernetesControlPlaneACLQuery, } from 'src/queries/kubernetes'; -import { ControlPlaneACLIPInputsV2 } from './ControlPlaneACLIPInputsV2'; - import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; interface Props { @@ -134,9 +133,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { } catch (errors) { for (const error of errors) { if (error.field) { - setError(error.field, { message: error.message }); + setError(error.field, { message: error.reason }); } else { - setError('root', { message: error.message }); + setError('root', { message: error.reason }); } } } @@ -227,18 +226,32 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { {errors.acl.message} )} - - setValue('acl.addresses.ipv4', ips, { shouldDirty: true }) - } - handleIPv6Change={(ips: string[]) => - setValue('acl.addresses.ipv6', ips, { shouldDirty: true }) - } - ipV4Addr={acl.addresses?.ipv4 ?? []} - ipV6Addr={acl.addresses?.ipv6 ?? []} - ipv4Errors={errors.acl?.addresses?.ipv4} - ipv6Errors={errors.acl?.addresses?.ipv6} - /> + + + setValue('acl.addresses.ipv4', ips, { shouldDirty: true }) + } + buttonText="Add IPv4 Address" + ipErrors={errors.acl?.addresses?.ipv4} + isLinkStyled + nonExtendedIPs={acl.addresses?.ipv4 ?? ['']} + placeholder="0.0.0.0/0" + title="IPv4 Addresses or CIDRs" + /> + + + setValue('acl.addresses.ipv6', ips, { shouldDirty: true }) + } + buttonText="Add IPv6 Address" + ipErrors={errors.acl?.addresses?.ipv6} + isLinkStyled + nonExtendedIPs={acl.addresses?.ipv6 ?? ['']} + placeholder="::/0" + title="IPv6 Addresses or CIDRs" + /> + + Date: Tue, 15 Oct 2024 15:42:07 -0400 Subject: [PATCH 27/36] cleanup --- .../CreateCluster/ControlPlaneACLIPInputs.tsx | 57 ------------------ .../CreateCluster/ControlPlaneACLPane.tsx | 59 +++++++++++-------- .../KubeControlPaneACLDrawer.tsx | 24 +++----- .../pr-10968-added-1729020457987.md | 5 ++ packages/validation/src/kubernetes.schema.ts | 36 +---------- 5 files changed, 51 insertions(+), 130 deletions(-) delete mode 100644 packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLIPInputs.tsx create mode 100644 packages/validation/.changeset/pr-10968-added-1729020457987.md diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLIPInputs.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLIPInputs.tsx deleted file mode 100644 index eb764ffafe9..00000000000 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLIPInputs.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Box } from '@mui/material'; -import * as React from 'react'; - -import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; - -import type { ExtendedIP } from 'src/utilities/ipUtils'; - -interface Props { - handleIPv4Blur: (ips: ExtendedIP[]) => void; - handleIPv4Change: (ips: ExtendedIP[]) => void; - handleIPv6Blur: (ips: ExtendedIP[]) => void; - handleIPv6Change: (ips: ExtendedIP[]) => void; - ipV4Addr: ExtendedIP[]; - ipV6Addr: ExtendedIP[]; - marginAfter?: boolean; -} - -export const ControlPlaneACLIPInputs = (props: Props) => { - const { - handleIPv4Blur, - handleIPv4Change, - handleIPv6Blur, - handleIPv6Change, - ipV4Addr, - ipV6Addr, - marginAfter, - } = props; - - const outerSx = marginAfter - ? { marginBottom: 3, maxWidth: 450 } - : { maxWidth: 450 }; - - return ( - - - - - - - ); -}; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index fb52ba56c51..9820cb7f31e 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -1,17 +1,17 @@ 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 { Link } from 'src/components/Link'; +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 { ControlPlaneACLIPInputs } from './ControlPlaneACLIPInputs'; - import type { ExtendedIP } from 'src/utilities/ipUtils'; export interface ControlPlaneACLProps { @@ -66,27 +66,40 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { /> {enableControlPlaneACL && ( - { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv4 address.', - }); - handleIPv4Change(validatedIPs); - }} - handleIPv6Blur={(_ips: ExtendedIP[]) => { - const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, - errorMessage: 'Must be a valid IPv6 address.', - }); - handleIPv6Change(validatedIPs); - }} - handleIPv4Change={handleIPv4Change} - handleIPv6Change={handleIPv4Change} - ipV4Addr={ipV4Addr} - ipV6Addr={ipV6Addr} - marginAfter - /> + + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + 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" + /> + + { + const validatedIPs = validateIPs(_ips, { + allowEmptyAddress: false, + 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" + /> + + )} ); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 1757ad4a5bc..f19df8f31f2 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -1,11 +1,11 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { kubernetesControlPlaneACLPayloadSchema } from '@linode/validation'; -import { Box } from '@mui/material'; import { Divider, Stack } from '@mui/material'; import * as React from 'react'; import { useForm } from 'react-hook-form'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Box } from 'src/components/Box'; import { Drawer } from 'src/components/Drawer'; import { DrawerContent } from 'src/components/DrawerContent'; import { FormControlLabel } from 'src/components/FormControlLabel'; @@ -95,16 +95,12 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { // // - Hopefully this explains the behavior of this code, and why one must be very careful // before introducing any clever/streamlined code - there's a reason to the mess :) - // - // if (acl.ipv4.some((ip) => ip.error) || acl.ipv6.some((ip) => ip.error)) { - // return; - // } - const _ipv4 = acl.addresses?.ipv4 + const ipv4 = acl.addresses?.ipv4 ? acl.addresses.ipv4.filter((ip) => ip !== '') : []; - const _ipv6 = acl.addresses?.ipv6 + const ipv6 = acl.addresses?.ipv6 ? acl.addresses.ipv6.filter((ip) => ip !== '') : []; @@ -112,10 +108,10 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { acl: { enabled: acl.enabled, 'revision-id': acl['revision-id'], - ...((_ipv4.length > 0 || _ipv6.length > 0) && { + ...((ipv4.length > 0 || ipv6.length > 0) && { addresses: { - ...(_ipv4.length > 0 && { ipv4: _ipv4 }), - ...(_ipv6.length > 0 && { ipv6: _ipv6 }), + ...(ipv4.length > 0 && { ipv4 }), + ...(ipv6.length > 0 && { ipv6 }), }, }), }, @@ -132,17 +128,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { closeDrawer(); } catch (errors) { for (const error of errors) { - if (error.field) { - setError(error.field, { message: error.reason }); - } else { - setError('root', { message: error.reason }); - } + setError(error?.field ?? 'root', { message: error.reason }); } } }; - // console.log('the errors', values, errors); - return ( { - if (value !== '' && value === undefined) { - return false; - } - - return ( - value === '' || - ipaddr.IPv4.isValid(value) || - ipaddr.IPv4.isValidCIDR(value) - ); - }, -}); - -const ipv6Validation = string().test({ - message: 'Must be a valid IPv6 address.', - test: (value) => { - if (value !== '' && value === undefined) { - return false; - } - - return ( - value === '' || - ipaddr.IPv6.isValid(value) || - ipaddr.IPv6.isValidCIDR(value) - ); - }, -}); - const controlPlaneACLOptionsSchema = object().shape({ enabled: boolean(), 'revision-id': string(), addresses: object().shape({ - ipv4: array().of(ipv4Validation), - ipv6: array().of(ipv6Validation), + ipv4: array().of(ipAddress), + ipv6: array().of(ipAddress), }), }); From 6bc1260278f4ef01c9f676f3619b1496760d6a25 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 15 Oct 2024 16:20:13 -0400 Subject: [PATCH 28/36] fix validation schema --- packages/validation/src/firewalls.schema.ts | 3 ++- packages/validation/src/kubernetes.schema.ts | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index e1a2eeb6e4f..d00e1eb6451 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -7,7 +7,8 @@ export const IP_ERROR_MESSAGE = 'Must be a valid IPv4 or IPv6 address or range.'; export const validateIP = (ipAddress?: string | null): boolean => { - if (!ipAddress) { + // ''is falsy, so we must specify that it is OK + if (ipAddress !== '' && !ipAddress) { return false; } // We accept plain IPs as well as ranges (i.e. CIDR notation). Ipaddr.js has separate parsing diff --git a/packages/validation/src/kubernetes.schema.ts b/packages/validation/src/kubernetes.schema.ts index 7d6a2b7200e..8910936d005 100644 --- a/packages/validation/src/kubernetes.schema.ts +++ b/packages/validation/src/kubernetes.schema.ts @@ -1,4 +1,4 @@ -import { ipAddress } from './firewalls.schema'; +import { validateIP } from './firewalls.schema'; import { array, number, object, string, boolean } from 'yup'; export const nodePoolSchema = object().shape({ @@ -60,12 +60,24 @@ export const createKubeClusterSchema = object().shape({ .min(1, 'Please add at least one node pool.'), }); +export const ipv4Address = string().test({ + name: 'validateIP', + message: 'Must be a valid IPv4 address.', + test: validateIP, +}); + +export const ipv6Address = string().test({ + name: 'validateIP', + message: 'Must be a valid IPv6 address.', + test: validateIP, +}); + const controlPlaneACLOptionsSchema = object().shape({ enabled: boolean(), 'revision-id': string(), addresses: object().shape({ - ipv4: array().of(ipAddress), - ipv6: array().of(ipAddress), + ipv4: array().of(ipv4Address).nullable(true), + ipv6: array().of(ipv6Address).nullable(true), }), }); From c0af13636b1a16cd7970ba53538fc4dfd99eed0a Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 16 Oct 2024 15:39:52 -0400 Subject: [PATCH 29/36] allow empty addresses --- .../CreateCluster/ControlPlaneACLPane.tsx | 4 +- .../KubeControlPaneACLDrawer.tsx | 106 ++++++++++-------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index 9820cb7f31e..bf9c457f510 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -70,7 +70,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { { const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, + allowEmptyAddress: true, errorMessage: 'Must be a valid IPv4 address.', }); handleIPv4Change(validatedIPs); @@ -86,7 +86,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { { const validatedIPs = validateIPs(_ips, { - allowEmptyAddress: false, + allowEmptyAddress: true, errorMessage: 'Must be a valid IPv6 address.', }); handleIPv6Change(validatedIPs); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index f19df8f31f2..ec8df95ab6a 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -2,7 +2,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { kubernetesControlPlaneACLPayloadSchema } from '@linode/validation'; import { Divider, Stack } from '@mui/material'; import * as React from 'react'; -import { useForm } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Box } from 'src/components/Box'; @@ -48,11 +48,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { ); const { + control, formState: { errors, isDirty, isSubmitting }, handleSubmit, reset, setError, - setValue, watch, } = useForm({ defaultValues: data, @@ -70,8 +70,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { }, }); - const values = watch(); - const { acl } = values; + const { acl } = watch(); const updateCluster = async () => { // A quick note on the following code: @@ -168,19 +167,22 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { only be accessible through the defined IP CIDRs. - { - setValue('acl.enabled', e.target.checked, { - shouldDirty: true, - }); - }} - checked={acl.enabled ?? false} - name="ipacl-checkbox" + ( + + } + label={'IPACL Enabled'} /> - } - label={'IPACL Enabled'} + )} + control={control} + name="acl.enabled" /> @@ -192,16 +194,18 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { and enforcements. Optional field. If omitted, defaults to a randomly generated string. - - setValue('acl.revision-id', e.target.value, { - shouldDirty: true, - }) - } - data-qa-label-input - errorText={errors.acl?.['revision-id']?.message} - label="Revision ID" - value={acl['revision-id']} + ( + + )} + control={control} + name="acl.revision-id" /> @@ -217,28 +221,38 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} - - setValue('acl.addresses.ipv4', ips, { shouldDirty: true }) - } - buttonText="Add IPv4 Address" - ipErrors={errors.acl?.addresses?.ipv4} - isLinkStyled - nonExtendedIPs={acl.addresses?.ipv4 ?? ['']} - placeholder="0.0.0.0/0" - title="IPv4 Addresses or CIDRs" + ( + + )} + control={control} + name="acl.addresses.ipv4" /> - - setValue('acl.addresses.ipv6', ips, { shouldDirty: true }) - } - buttonText="Add IPv6 Address" - ipErrors={errors.acl?.addresses?.ipv6} - isLinkStyled - nonExtendedIPs={acl.addresses?.ipv6 ?? ['']} - placeholder="::/0" - title="IPv6 Addresses or CIDRs" + ( + + )} + control={control} + name="acl.addresses.ipv6" /> From af7080db6bd7c9cbc0002598597f4b4ea78cad3a Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Wed, 16 Oct 2024 23:36:38 -0400 Subject: [PATCH 30/36] update copies to most recent, add enabled to query --- .../CreateCluster/ControlPlaneACLPane.tsx | 13 ++-- .../CreateCluster/CreateCluster.tsx | 2 +- .../KubeControlPaneACLDrawer.tsx | 51 ++++++++----- .../KubeSummaryPanel.tsx | 73 +++++++++++-------- packages/manager/src/queries/kubernetes.ts | 12 ++- 5 files changed, 87 insertions(+), 64 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index bf9c457f510..be10d816416 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -5,7 +5,6 @@ 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 { Link } from 'src/components/Link'; import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; import { Notice } from 'src/components/Notice/Notice'; import { Toggle } from 'src/components/Toggle/Toggle'; @@ -40,7 +39,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { - Control Plane Access Control (IPACL) + Control Plane ACL (Access Control List) {errorText && ( @@ -49,10 +48,10 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { )} - This is the text for Control Plane Access Control.{' '} - - Learn more. - + 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. { onChange={() => setControlPlaneACL(!enableControlPlaneACL)} /> } - label="Enable IPACL" + label="Enable Control Plane ACL" /> {enableControlPlaneACL && ( diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 07e1ae89f76..21a39a48c74 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -352,7 +352,7 @@ export const CreateCluster = () => { )} - + {showHighAvailability && ( { - const { closeDrawer, clusterId, clusterLabel, clusterMigrated, open } = props; + const { + closeDrawer, + clusterId, + clusterLabel, + clusterMigrated, + open, + showControlPlaneACL, + } = props; const { data: data, error: isErrorKubernetesACL, isLoading: isLoadingKubernetesACL, - } = useKubernetesControlPlaneACLQuery(clusterId); + } = useKubernetesControlPlaneACLQuery(clusterId, showControlPlaneACL); const { mutateAsync: updateKubernetesClusterControlPlaneACL, @@ -137,7 +145,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { onClose={closeDrawer} onExited={() => reset()} open={open} - title={'Control Plane Access Control List'} + title={'Control Plane ACL'} wide > { )} - When a cluster is equipped with an ACL, the apiserver and - dashboard endpoints get mapped to a NodeBalancer address where all - traffic is protected through a Cloud Firewall. + Control Plane ACL secures network access to your LKE cluster's + control plane. When not enabled, any public IP address can be used + to access your control plane. When enabled, all network access is + denied except for the IP addresses and CIDR ranges defined on the + ACL. - Enabled + Control Plane ACL - A value of true results in a default policy of DENY. A value of - false results in a default policy of ALLOW (i.e., access controls - are disabled). When enabled, control plane access controls can - only be accessible through the defined IP CIDRs. + Once enabled, all network access is denied except for the IP + addresses and CIDR ranges defined on the ACL. { onChange={field.onChange} /> } - label={'IPACL Enabled'} + label={'Enable Control Plane ACL'} /> )} control={control} @@ -190,9 +198,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { <> Revision ID - Enables clients to track events related to ACL update requests - and enforcements. Optional field. If omitted, defaults to a - randomly generated string. + A unique identifing string for this particular revision to the + ACL, used by clients to track events related to ACL update + requests and enforcement. This defaults to a randomly + generated string but can be edited if you prefer to specify + your own string to use for tracking this change. ( @@ -212,8 +222,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} Addresses - A list of individual ipv4 and ipv6 addresses or CIDRs to ALLOW - access to the control plane. + A list of allowed IPv4 and IPv6 addresses and ranges in CIDR + notion. This cluster's control plane will only be accessible from + IP addresses within this list. {errors.acl?.message && clusterMigrated && ( @@ -268,9 +279,9 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { /> {!clusterMigrated && ( - IPACL has not yet been installed on this cluster. During - installation, it may take up to 20 minutes before ACLs are fully - enforced for the first time. + Control Plane ACL has not yet been installed on this cluster. + During installation, it may take up to 15 minutes for the access + control list to be fully enforced. )} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index ac38a5cd731..9374775714f 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -17,7 +17,9 @@ import { Stack } from 'src/components/Stack'; import { TagCell } from 'src/components/TagCell/TagCell'; import { Typography } from 'src/components/Typography'; import { KubeClusterSpecs } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs'; +import { getKubeControlPlaneACL } from 'src/features/Kubernetes/kubeUtils'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; +import { useAccount } from 'src/queries/account/account'; import { useKubernetesClusterMutation, useKubernetesControlPlaneACLQuery, @@ -46,6 +48,9 @@ interface Props { export const KubeSummaryPanel = React.memo((props: Props) => { const { cluster } = props; + const { data: account } = useAccount(); + const { showControlPlaneACL } = getKubeControlPlaneACL(account); + const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); @@ -82,7 +87,7 @@ export const KubeSummaryPanel = React.memo((props: Props) => { data: aclData, error: isErrorKubernetesACL, isLoading: isLoadingKubernetesACL, - } = useKubernetesControlPlaneACLQuery(cluster.id); + } = useKubernetesControlPlaneACLQuery(cluster.id, !!showControlPlaneACL); const enabledACL = aclData?.acl.enabled ?? false; const totalIPv4 = aclData?.acl.addresses?.ipv4?.length ?? 0; @@ -177,38 +182,41 @@ export const KubeSummaryPanel = React.memo((props: Props) => { } footer={ - - - - Control Plane ACL: - - {isLoadingKubernetesACL ? ( - - - - ) : ( - setControlPlaneACLDrawerOpen(true)} + showControlPlaneACL ? ( + + + - {determineIPACLButtonCopy} - - )} - - + Control Plane ACL: + + {isLoadingKubernetesACL ? ( + + + + ) : ( + setControlPlaneACLDrawerOpen(true)} + > + {determineIPACLButtonCopy} + + )} + + + ) : undefined } header={ @@ -267,6 +275,7 @@ export const KubeSummaryPanel = React.memo((props: Props) => { clusterLabel={cluster.label} clusterMigrated={!isErrorKubernetesACL} open={isControlPlaneACLDrawerOpen} + showControlPlaneACL={!!showControlPlaneACL} /> { }); }; -export const useKubernetesControlPlaneACLQuery = (clusterId: number) => { - return useQuery( - kubernetesQueries.cluster(clusterId)._ctx.acl - ); +export const useKubernetesControlPlaneACLQuery = ( + clusterId: number, + enabled: boolean = true +) => { + return useQuery({ + enabled, + ...kubernetesQueries.cluster(clusterId)._ctx.acl, + }); }; export const useKubernetesControlPlaneACLMutation = (id: number) => { From 0188f60aaf59ed679997361085b9341e64b30661 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 17 Oct 2024 10:59:17 -0400 Subject: [PATCH 31/36] add footer, address feedback, tag bug --- .../KubeControlPaneACLDrawer.tsx | 4 +- .../KubeEntityDetailFooter.tsx | 185 ++++++++++++++++++ .../KubeSummaryPanel.styles.tsx | 53 ----- .../KubeSummaryPanel.tsx | 87 ++------ .../Linodes/LinodeEntityDetail.styles.ts | 5 +- packages/manager/src/queries/kubernetes.ts | 6 +- 6 files changed, 207 insertions(+), 133 deletions(-) create mode 100644 packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 132c7225bc8..dffddbb9ab9 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -226,8 +226,8 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { notion. This cluster's control plane will only be accessible from IP addresses within this list. - {errors.acl?.message && clusterMigrated && ( - + {errors.acl?.message && ( + {errors.acl.message} )} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx new file mode 100644 index 00000000000..d861b201a0f --- /dev/null +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx @@ -0,0 +1,185 @@ +import { useTheme } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; +import { useSnackbar } from 'notistack'; +import * as React from 'react'; + +import { Box } from 'src/components/Box'; +import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; +import { CircleProgress } from 'src/components/CircleProgress'; +import { TagCell } from 'src/components/TagCell/TagCell'; +import { + StyledBox, + StyledLabelBox, + StyledListItem, + sxLastListItem, + sxListItemFirstChild, +} from 'src/features/Linodes/LinodeEntityDetail.styles'; +import { useKubernetesClusterMutation } from 'src/queries/kubernetes'; +import { useProfile } from 'src/queries/profile/profile'; +import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; +import { formatDate } from 'src/utilities/formatDate'; +import { pluralize } from 'src/utilities/pluralize'; + +import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; + +interface FooterProps { + aclData: KubernetesControlPlaneACLPayload | undefined; + clusterCreated: string; + clusterId: number; + clusterLabel: string; + clusterTags: string[]; + clusterUpdated: string; + isClusterReadOnly: boolean; + isLoadingKubernetesACL: boolean; + setControlPlaneACLDrawerOpen: React.Dispatch>; + showControlPlaneACL: boolean; +} + +export const KubeEntityDetailFooter = React.memo((props: FooterProps) => { + const theme = useTheme(); + + const { data: profile } = useProfile(); + const { + aclData, + clusterCreated, + clusterId, + clusterLabel, + clusterTags, + clusterUpdated, + isClusterReadOnly, + isLoadingKubernetesACL, + setControlPlaneACLDrawerOpen, + showControlPlaneACL, + } = props; + + const enabledACL = aclData?.acl.enabled ?? false; + const totalIPv4 = aclData?.acl.addresses?.ipv4?.length ?? 0; + const totalIPv6 = aclData?.acl.addresses?.ipv6?.length ?? 0; + const totalNumberIPs = totalIPv4 + totalIPv6; + + const buttonCopyACL = enabledACL + ? `Enabled (${pluralize('IP Address', 'IP Addresses', totalNumberIPs)})` + : 'Enable'; + + const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( + clusterId + ); + + const { enqueueSnackbar } = useSnackbar(); + + const handleUpdateTags = React.useCallback( + (newTags: string[]) => { + return updateKubernetesCluster({ + tags: newTags, + }).catch((e) => + enqueueSnackbar( + getAPIErrorOrDefault(e, 'Error updating tags')[0].reason, + { + variant: 'error', + } + ) + ); + }, + [updateKubernetesCluster, enqueueSnackbar] + ); + + return ( + + + + + Cluster ID:{' '} + {clusterId} + + {showControlPlaneACL && ( + + + Control Plane ACL:{' '} + {' '} + {isLoadingKubernetesACL ? ( + + + + ) : ( + setControlPlaneACLDrawerOpen(true)} + > + {buttonCopyACL} + + )} + + )} + + + + Created:{' '} + {formatDate(clusterCreated, { + timezone: profile?.timezone, + })} + + + Updated:{' '} + {formatDate(clusterUpdated, { + timezone: profile?.timezone, + })} + + + + + + + + ); +}); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx index bee54c5e09f..3dc8e3799e9 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.styles.tsx @@ -2,20 +2,6 @@ import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; -import { Box } from 'src/components/Box'; - -export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ - alignItems: 'center', - display: 'flex', - [theme.breakpoints.down('lg')]: { - minHeight: theme.spacing(3), - }, - [theme.breakpoints.up('lg')]: { - minHeight: theme.spacing(5), - padding: `0px 10px`, - }, -})); - export const StyledActionRowGrid = styled(Grid, { label: 'StyledActionRowGrid', })({ @@ -29,42 +15,3 @@ export const StyledActionRowGrid = styled(Grid, { justifyContent: 'flex-end', padding: '8px 0px', }); - -export const StyledTagGrid = styled(Grid, { label: 'StyledTagGrid' })( - ({ theme }) => ({ - // Tags Panel wrapper - '& > div:last-child': { - marginBottom: 0, - marginTop: 2, - width: '100%', - }, - '&.MuiGrid-item': { - paddingBottom: 0, - }, - alignItems: 'flex-end', - alignSelf: 'stretch', - display: 'flex', - flexDirection: 'column', - justifyContent: 'flex-end', - [theme.breakpoints.down('lg')]: { - width: '100%', - }, - [theme.breakpoints.up('lg')]: { - '& .MuiChip-root': { - marginLeft: 4, - marginRight: 0, - }, - // Add a Tag button - '& > div:first-of-type': { - justifyContent: 'flex-end', - marginTop: theme.spacing(4), - }, - // Tags Panel wrapper - '& > div:last-child': { - display: 'flex', - justifyContent: 'flex-end', - }, - }, - width: '100%', - }) -); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx index 9374775714f..a4cca466c65 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeSummaryPanel.tsx @@ -7,37 +7,29 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Box } from 'src/components/Box'; import { StyledActionButton } from 'src/components/Button/StyledActionButton'; -import { StyledLinkButton } from 'src/components/Button/StyledLinkButton'; import { Chip } from 'src/components/Chip'; -import { CircleProgress } from 'src/components/CircleProgress'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { EntityDetail } from 'src/components/EntityDetail/EntityDetail'; import { EntityHeader } from 'src/components/EntityHeader/EntityHeader'; import { Stack } from 'src/components/Stack'; -import { TagCell } from 'src/components/TagCell/TagCell'; import { Typography } from 'src/components/Typography'; import { KubeClusterSpecs } from 'src/features/Kubernetes/KubernetesClusterDetail/KubeClusterSpecs'; import { getKubeControlPlaneACL } from 'src/features/Kubernetes/kubeUtils'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useAccount } from 'src/queries/account/account'; import { - useKubernetesClusterMutation, useKubernetesControlPlaneACLQuery, useKubernetesDashboardQuery, useResetKubeConfigMutation, } from 'src/queries/kubernetes'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; -import { pluralize } from 'src/utilities/pluralize'; import { DeleteKubernetesClusterDialog } from './DeleteKubernetesClusterDialog'; import { KubeConfigDisplay } from './KubeConfigDisplay'; import { KubeConfigDrawer } from './KubeConfigDrawer'; import { KubeControlPlaneACLDrawer } from './KubeControlPaneACLDrawer'; -import { - StyledActionRowGrid, - StyledBox, - StyledTagGrid, -} from './KubeSummaryPanel.styles'; +import { KubeEntityDetailFooter } from './KubeEntityDetailFooter'; +import { StyledActionRowGrid } from './KubeSummaryPanel.styles'; import type { KubernetesCluster } from '@linode/api-v4/lib/kubernetes'; @@ -62,10 +54,6 @@ export const KubeSummaryPanel = React.memo((props: Props) => { ] = React.useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); - const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( - cluster.id - ); - const { data: dashboard, error: dashboardError, @@ -89,15 +77,6 @@ export const KubeSummaryPanel = React.memo((props: Props) => { isLoading: isLoadingKubernetesACL, } = useKubernetesControlPlaneACLQuery(cluster.id, !!showControlPlaneACL); - const enabledACL = aclData?.acl.enabled ?? false; - const totalIPv4 = aclData?.acl.addresses?.ipv4?.length ?? 0; - const totalIPv6 = aclData?.acl.addresses?.ipv6?.length ?? 0; - const totalNumberIPs = totalIPv4 + totalIPv6; - - const determineIPACLButtonCopy = enabledACL - ? pluralize('IP Address', 'IP Addresses', totalNumberIPs) - : 'Enable'; - const [ resetKubeConfigDialogOpen, setResetKubeConfigDialogOpen, @@ -116,12 +95,6 @@ export const KubeSummaryPanel = React.memo((props: Props) => { setDrawerOpen(true); }; - const handleUpdateTags = (newTags: string[]) => { - return updateKubernetesCluster({ - tags: newTags, - }); - }; - const sxSpacing = { paddingLeft: theme.spacing(3), paddingRight: theme.spacing(1), @@ -169,54 +142,22 @@ export const KubeSummaryPanel = React.memo((props: Props) => { /> )} - - - } footer={ - showControlPlaneACL ? ( - - - - Control Plane ACL: - - {isLoadingKubernetesACL ? ( - - - - ) : ( - setControlPlaneACLDrawerOpen(true)} - > - {determineIPACLButtonCopy} - - )} - - - ) : undefined + } header={ diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts b/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts index 4c7ec07a99e..47f29f03e5b 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts @@ -1,8 +1,7 @@ // This component was built asuming an unmodified MUI
+import { styled } from '@mui/material/styles'; import Table from '@mui/material/Table'; import Grid from '@mui/material/Unstable_Grid2'; -import { styled } from '@mui/material/styles'; -import { Theme } from '@mui/material/styles'; import { Link } from 'react-router-dom'; import { Box } from 'src/components/Box'; @@ -11,6 +10,8 @@ import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; +import type { Theme } from '@mui/material/styles'; + // --------------------------------------------------------------------- // Header Styles // --------------------------------------------------------------------- diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index 5c636fdcd51..bd500b06dfb 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -146,6 +146,9 @@ export const useKubernetesClusterMutation = (id: number) => { queryClient.invalidateQueries({ queryKey: kubernetesQueries.lists.queryKey, }); + queryClient.invalidateQueries({ + queryKey: kubernetesQueries.cluster(id).queryKey, + }); queryClient.invalidateQueries({ queryKey: kubernetesQueries.cluster(id)._ctx.acl.queryKey, }); @@ -378,9 +381,6 @@ export const useKubernetesControlPlaneACLMutation = (id: number) => { >({ mutationFn: (data) => updateKubernetesClusterControlPlaneACL(id, data), onSuccess(data) { - queryClient.invalidateQueries({ - queryKey: kubernetesQueries.cluster(id)._ctx.acl.queryKey, - }); queryClient.setQueryData( kubernetesQueries.cluster(id)._ctx.acl.queryKey, data From 1582ac5a05d92f519b890689cd416026f722d143 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 17 Oct 2024 12:42:56 -0400 Subject: [PATCH 32/36] address feedback + quick design feedback --- .../CreateCluster/ControlPlaneACLPane.tsx | 2 +- .../KubeControlPaneACLDrawer.tsx | 14 +++++----- .../KubeEntityDetailFooter.tsx | 1 + .../KubernetesClusterDetail.tsx | 7 +---- packages/manager/src/queries/kubernetes.ts | 26 ++++++------------- 5 files changed, 18 insertions(+), 32 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index be10d816416..0b515b4889f 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -47,7 +47,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { {' '} )} - + 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 diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index dffddbb9ab9..a1455c34e2e 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -161,7 +161,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} - + Control Plane ACL secures network access to your LKE cluster's control plane. When not enabled, any public IP address can be used to access your control plane. When enabled, all network access is @@ -170,7 +170,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { Control Plane ACL - + Once enabled, all network access is denied except for the IP addresses and CIDR ranges defined on the ACL. @@ -197,7 +197,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { {clusterMigrated && ( <> Revision ID - + A unique identifing string for this particular revision to the ACL, used by clients to track events related to ACL update requests and enforcement. This defaults to a randomly @@ -221,10 +221,10 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} Addresses - - A list of allowed IPv4 and IPv6 addresses and ranges in CIDR - notion. This cluster's control plane will only be accessible from - IP addresses within this list. + + A list of allowed IPv4 and IPv6 addresses and CIDR ranges. This + cluster's control plane will only be accessible from IP addresses + within this list. {errors.acl?.message && ( diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx index d861b201a0f..7093d838879 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeEntityDetailFooter.tsx @@ -97,6 +97,7 @@ export const KubeEntityDetailFooter = React.memo((props: FooterProps) => { > { const id = Number(clusterID); const location = useLocation(); const showAPL = useGetAPLAvailability(); - const kubernetesClusterBetaQuery = useKubernetesClusterBetaQuery(id); - const kubernetesClusterQuery = useKubernetesClusterQuery(id); - const { data: cluster, error, isLoading } = showAPL - ? kubernetesClusterBetaQuery - : kubernetesClusterQuery; + const { data: cluster, error, isLoading } = useKubernetesClusterQuery(id); const { data: regionsData } = useRegionsQuery(); const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index bd500b06dfb..688eb6d265e 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -30,6 +30,7 @@ import { useQueryClient, } from '@tanstack/react-query'; +import { useGetAPLAvailability } from 'src/features/Kubernetes/kubeUtils'; import { getAll } from 'src/utilities/getAll'; import { queryPresets } from './base'; @@ -61,10 +62,6 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { queryFn: () => getKubernetesClusterControlPlaneACL(id), queryKey: [id], }, - beta: { - queryFn: () => getKubernetesClusterBeta(id), - queryKey: [id], - }, dashboard: { queryFn: () => getKubernetesClusterDashboard(id), queryKey: null, @@ -112,17 +109,13 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { }); export const useKubernetesClusterQuery = (id: number) => { - return useQuery(kubernetesQueries.cluster(id)); -}; - -/** - * duplicated function of useKubernetesClusterQuery - * necessary to call BETA_API_ROOT in a seperate function based on feature flag - */ -export const useKubernetesClusterBetaQuery = (id: number) => { - return useQuery( - kubernetesQueries.cluster(id)._ctx.beta - ); + const showAPL = useGetAPLAvailability(); + return useQuery({ + ...kubernetesQueries.cluster(id), + queryFn: showAPL + ? () => getKubernetesClusterBeta(id) // necessary to call BETA_API_ROOT in a seperate function based on feature flag + : () => getKubernetesCluster(id), + }); }; export const useKubernetesClustersQuery = ( @@ -146,9 +139,6 @@ export const useKubernetesClusterMutation = (id: number) => { queryClient.invalidateQueries({ queryKey: kubernetesQueries.lists.queryKey, }); - queryClient.invalidateQueries({ - queryKey: kubernetesQueries.cluster(id).queryKey, - }); queryClient.invalidateQueries({ queryKey: kubernetesQueries.cluster(id)._ctx.acl.queryKey, }); From c8715701c318667cd9bb18deadf434a62f7c717c Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 17 Oct 2024 12:47:42 -0400 Subject: [PATCH 33/36] design feedback quick change --- .../KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index a1455c34e2e..8eb7441c876 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -161,7 +161,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} - + Control Plane ACL secures network access to your LKE cluster's control plane. When not enabled, any public IP address can be used to access your control plane. When enabled, all network access is @@ -170,7 +170,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { Control Plane ACL - + Once enabled, all network access is denied except for the IP addresses and CIDR ranges defined on the ACL. @@ -197,7 +197,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { {clusterMigrated && ( <> Revision ID - + A unique identifing string for this particular revision to the ACL, used by clients to track events related to ACL update requests and enforcement. This defaults to a randomly @@ -221,7 +221,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} Addresses - + A list of allowed IPv4 and IPv6 addresses and CIDR ranges. This cluster's control plane will only be accessible from IP addresses within this list. From b9e12c70f66090344d5ed791467174c300e0ab6d Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Thu, 17 Oct 2024 13:19:48 -0400 Subject: [PATCH 34/36] notice style update --- .../KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 8eb7441c876..d1b40092c9c 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -279,9 +279,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { /> {!clusterMigrated && ( - Control Plane ACL has not yet been installed on this cluster. - During installation, it may take up to 15 minutes for the access - control list to be fully enforced. + + Control Plane ACL has not yet been installed on this cluster. + During installation, it may take up to 15 minutes for the + access control list to be fully enforced. + )} From b0d9a8af54da688c64afd26e8208b30c6933f3a1 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Mon, 21 Oct 2024 17:37:29 -0400 Subject: [PATCH 35/36] address feedback --- packages/api-v4/src/kubernetes/kubernetes.ts | 4 +-- .../KubeControlPaneACLDrawer.tsx | 30 ++++++++++++------- packages/manager/src/queries/kubernetes.ts | 1 + 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/api-v4/src/kubernetes/kubernetes.ts b/packages/api-v4/src/kubernetes/kubernetes.ts index 402d8633a92..03e46b53dc9 100644 --- a/packages/api-v4/src/kubernetes/kubernetes.ts +++ b/packages/api-v4/src/kubernetes/kubernetes.ts @@ -228,12 +228,12 @@ export const getKubernetesTypes = (params?: Params) => * * Return control plane access list about a single Kubernetes cluster */ -export const getKubernetesClusterControlPlaneACL = (clusterID: number) => +export const getKubernetesClusterControlPlaneACL = (clusterId: number) => Request( setMethod('GET'), setURL( `${API_ROOT}/lke/clusters/${encodeURIComponent( - clusterID + clusterId )}/control_plane_acl` ) ); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index d1b40092c9c..8e30a46473e 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -1,6 +1,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { kubernetesControlPlaneACLPayloadSchema } from '@linode/validation'; import { Divider, Stack } from '@mui/material'; +import { styled } from '@mui/material/styles'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -19,6 +20,7 @@ import { useKubernetesControlPlaneACLMutation, useKubernetesControlPlaneACLQuery, } from 'src/queries/kubernetes'; +import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; import type { KubernetesControlPlaneACLPayload } from '@linode/api-v4'; @@ -32,6 +34,7 @@ interface Props { } export const KubeControlPlaneACLDrawer = (props: Props) => { + const formContainerRef = React.useRef(null); const { closeDrawer, clusterId, @@ -137,6 +140,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { for (const error of errors) { setError(error?.field ?? 'root', { message: error.reason }); } + scrollErrorIntoViewV2(formContainerRef); } }; @@ -154,26 +158,26 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { loading={isLoadingKubernetesACL} title={clusterLabel} > - + {errors.root?.message && ( {errors.root.message} )} - + Control Plane ACL secures network access to your LKE cluster's control plane. When not enabled, any public IP address can be used to access your control plane. When enabled, all network access is denied except for the IP addresses and CIDR ranges defined on the ACL. - + Control Plane ACL - + Once enabled, all network access is denied except for the IP addresses and CIDR ranges defined on the ACL. - + ( @@ -197,13 +201,13 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { {clusterMigrated && ( <> Revision ID - + A unique identifing string for this particular revision to the ACL, used by clients to track events related to ACL update requests and enforcement. This defaults to a randomly generated string but can be edited if you prefer to specify your own string to use for tracking this change. - + ( { )} Addresses - + A list of allowed IPv4 and IPv6 addresses and CIDR ranges. This cluster's control plane will only be accessible from IP addresses within this list. - + {errors.acl?.message && ( {errors.acl.message} @@ -279,11 +283,11 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { /> {!clusterMigrated && ( - + Control Plane ACL has not yet been installed on this cluster. During installation, it may take up to 15 minutes for the access control list to be fully enforced. - + )} @@ -292,3 +296,7 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { ); }; + +const StyledTypography = styled(Typography, { label: 'StyledTypography' })({ + width: '90%', +}); diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index 688eb6d265e..cf8e1322ff9 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -358,6 +358,7 @@ export const useKubernetesControlPlaneACLQuery = ( ) => { return useQuery({ enabled, + retry: 1, ...kubernetesQueries.cluster(clusterId)._ctx.acl, }); }; From 169427affe674294b747d61da0ec00c0c0987999 Mon Sep 17 00:00:00 2001 From: Connie Liu Date: Tue, 22 Oct 2024 12:19:48 -0400 Subject: [PATCH 36/36] update copies --- .../CreateCluster/ControlPlaneACLPane.tsx | 4 +--- .../KubeControlPaneACLDrawer.tsx | 21 ++++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx index 0b515b4889f..8939e656d0c 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ControlPlaneACLPane.tsx @@ -38,9 +38,7 @@ export const ControlPlaneACLPane = (props: ControlPlaneACLProps) => { <> - - Control Plane ACL (Access Control List) - + Control Plane ACL {errorText && ( diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 8e30a46473e..72074b2bc94 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -166,17 +166,18 @@ export const KubeControlPlaneACLDrawer = (props: Props) => { )} - Control Plane ACL secures network access to your LKE cluster's - control plane. When not enabled, any public IP address can be used - to access your control plane. When enabled, all network access is - denied except for the IP addresses and CIDR ranges defined on the - ACL. + Control Plane ACL secures network access to your LKE + cluster's control plane. Use this form to enable or disable + the ACL on your LKE cluster, update the list of allowed IP + addresses, and adjust other settings. - Control Plane ACL + Activation Status - Once enabled, all network access is denied except for the IP - addresses and CIDR ranges defined on the ACL. + Enable or disable the Control Plane ACL. If the ACL is not + enabled, any public IP address can be used to access your control + plane. Once enabled, all network access is denied except for the + IP addresses and CIDR ranges defined on the ACL. { Addresses A list of allowed IPv4 and IPv6 addresses and CIDR ranges. This - cluster's control plane will only be accessible from IP addresses - within this list. + cluster's control plane will only be accessible from IP + addresses within this list. {errors.acl?.message && (