From 693eaf6a1c6ea84ca0eec81ed7228ff15553fe70 Mon Sep 17 00:00:00 2001 From: Talmai Oliveira Date: Thu, 19 Sep 2024 01:43:57 -0400 Subject: [PATCH 01/24] [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/24] [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/24] [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/24] [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/24] [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/24] [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/24] 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/24] [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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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 ? (