Skip to content

Commit a28916f

Browse files
change: [M3-9585] - Restricted users should not be allowed to add / update the Kubernetes Cluster (linode#12360)
* Begin adding isLkeClusterRestricted logic * Finish disabling rest of actions * Added changeset: Kubernetes cluster details to show restricted access warnings and disabled actions * Update packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> * Move static Notice outside render tree --------- Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com>
1 parent fdbaeb9 commit a28916f

File tree

9 files changed

+73
-7
lines changed

9 files changed

+73
-7
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Changed
3+
---
4+
5+
Kubernetes cluster details to show restricted access warnings and disabled actions ([#12360](https://github.com/linode/manager/pull/12360))

packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { useAccount, useRegionsQuery } from '@linode/queries';
2-
import { Box, CircleProgress, ErrorState, Stack } from '@linode/ui';
2+
import { Box, CircleProgress, ErrorState, Notice, Stack } from '@linode/ui';
33
import { useLocation, useParams } from '@tanstack/react-router';
44
import * as React from 'react';
55

66
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
77
import { LandingHeader } from 'src/components/LandingHeader';
8+
import { getRestrictedResourceText } from 'src/features/Account/utils';
89
import {
910
useAPLAvailability,
1011
useKubernetesBetaEndpoint,
1112
} from 'src/features/Kubernetes/kubeUtils';
1213
import { getKubeHighAvailability } from 'src/features/Kubernetes/kubeUtils';
14+
import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted';
1315
import {
1416
useKubernetesClusterMutation,
1517
useKubernetesClusterQuery,
@@ -22,6 +24,17 @@ import { NodePoolsDisplay } from './NodePoolsDisplay/NodePoolsDisplay';
2224
import { UpgradeKubernetesClusterToHADialog } from './UpgradeClusterDialog';
2325
import UpgradeKubernetesVersionBanner from './UpgradeKubernetesVersionBanner';
2426

27+
const restrictedLkeNotice = (
28+
<Notice
29+
text={getRestrictedResourceText({
30+
action: 'edit',
31+
resourceType: 'LKE Clusters',
32+
isSingular: true,
33+
})}
34+
variant="warning"
35+
/>
36+
);
37+
2538
export const KubernetesClusterDetail = () => {
2639
const { data: account } = useAccount();
2740
const { clusterId } = useParams({ from: '/kubernetes/clusters/$clusterId' });
@@ -46,6 +59,12 @@ export const KubernetesClusterDetail = () => {
4659
const { isClusterHighlyAvailable, showHighAvailability } =
4760
getKubeHighAvailability(account, cluster);
4861

62+
const isLkeClusterRestricted = useIsResourceRestricted({
63+
grantLevel: 'read_only',
64+
grantType: 'lkecluster',
65+
id: cluster?.id,
66+
});
67+
4968
const [updateError, setUpdateError] = React.useState<string | undefined>();
5069
const [isUpgradeToHAOpen, setIsUpgradeToHAOpen] = React.useState(false);
5170

@@ -91,6 +110,7 @@ export const KubernetesClusterDetail = () => {
91110
clusterTier={cluster?.tier ?? 'standard'} // TODO LKE: remove fallback once LKE-E is in GA and tier is required
92111
currentVersion={cluster?.k8s_version}
93112
/>
113+
{isLkeClusterRestricted && restrictedLkeNotice}
94114
<LandingHeader
95115
breadcrumbProps={{
96116
breadcrumbDataAttrs: { 'data-qa-breadcrumb': true },
@@ -134,6 +154,7 @@ export const KubernetesClusterDetail = () => {
134154
clusterLabel={cluster.label}
135155
clusterRegionId={cluster.region}
136156
clusterTier={cluster.tier ?? 'standard'}
157+
isLkeClusterRestricted={isLkeClusterRestricted}
137158
regionsData={regionsData || []}
138159
/>
139160
</Stack>

packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeActionMenu.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,24 @@ import type { Theme } from '@mui/material/styles';
1010

1111
interface Props {
1212
instanceLabel?: string;
13+
isLkeClusterRestricted: boolean;
1314
nodeId?: string;
1415
openRecycleNodeDialog: (nodeID: string, linodeLabel: string) => void;
1516
}
1617

1718
export const NodeActionMenu = (props: Props) => {
18-
const { instanceLabel, nodeId, openRecycleNodeDialog } = props;
19+
const {
20+
instanceLabel,
21+
isLkeClusterRestricted,
22+
nodeId,
23+
openRecycleNodeDialog,
24+
} = props;
1925
const theme = useTheme<Theme>();
2026
const matchesSmDown = useMediaQuery(theme.breakpoints.down('md'));
2127

2228
const actions = [
2329
{
24-
disabled: !nodeId || !instanceLabel,
30+
disabled: !nodeId || !instanceLabel || isLkeClusterRestricted,
2531
onClick: () => {
2632
if (!nodeId || !instanceLabel) {
2733
return;

packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface Props {
3535
handleClickAutoscale: (poolId: number) => void;
3636
handleClickLabelsAndTaints: (poolId: number) => void;
3737
handleClickResize: (poolId: number) => void;
38+
isLkeClusterRestricted: boolean;
3839
isOnlyNodePool: boolean;
3940
nodes: PoolNodeResponse[];
4041
openDeletePoolDialog: (poolId: number) => void;
@@ -60,6 +61,7 @@ export const NodePool = (props: Props) => {
6061
handleClickAutoscale,
6162
handleClickLabelsAndTaints,
6263
handleClickResize,
64+
isLkeClusterRestricted,
6365
isOnlyNodePool,
6466
nodes,
6567
openDeletePoolDialog,
@@ -116,23 +118,27 @@ export const NodePool = (props: Props) => {
116118
<ActionMenu
117119
actionsList={[
118120
{
121+
disabled: isLkeClusterRestricted,
119122
onClick: () => handleClickLabelsAndTaints(poolId),
120123
title: 'Labels and Taints',
121124
},
122125
{
126+
disabled: isLkeClusterRestricted,
123127
onClick: () => handleClickAutoscale(poolId),
124128
title: 'Autoscale Pool',
125129
},
126130
{
131+
disabled: isLkeClusterRestricted,
127132
onClick: () => handleClickResize(poolId),
128133
title: 'Resize Pool',
129134
},
130135
{
136+
disabled: isLkeClusterRestricted,
131137
onClick: () => openRecycleAllNodesDialog(poolId),
132138
title: 'Recycle Pool Nodes',
133139
},
134140
{
135-
disabled: isOnlyNodePool,
141+
disabled: isLkeClusterRestricted || isOnlyNodePool,
136142
onClick: () => openDeletePoolDialog(poolId),
137143
title: 'Delete Pool',
138144
tooltip: isOnlyNodePool
@@ -155,6 +161,7 @@ export const NodePool = (props: Props) => {
155161
>
156162
<StyledActionButton
157163
compactY
164+
disabled={isLkeClusterRestricted}
158165
onClick={(e) => {
159166
e.stopPropagation();
160167
handleClickLabelsAndTaints(poolId);
@@ -164,6 +171,7 @@ export const NodePool = (props: Props) => {
164171
</StyledActionButton>
165172
<StyledActionButton
166173
compactY
174+
disabled={isLkeClusterRestricted}
167175
onClick={(e) => {
168176
e.stopPropagation();
169177
handleClickAutoscale(poolId);
@@ -178,6 +186,7 @@ export const NodePool = (props: Props) => {
178186
)}
179187
<StyledActionButton
180188
compactY
189+
disabled={isLkeClusterRestricted}
181190
onClick={(e) => {
182191
e.stopPropagation();
183192
handleClickResize(poolId);
@@ -187,6 +196,7 @@ export const NodePool = (props: Props) => {
187196
</StyledActionButton>
188197
<StyledActionButton
189198
compactY
199+
disabled={isLkeClusterRestricted}
190200
onClick={(e) => {
191201
e.stopPropagation();
192202
openRecycleAllNodesDialog(poolId);
@@ -204,7 +214,7 @@ export const NodePool = (props: Props) => {
204214
<div>
205215
<StyledActionButton
206216
compactY
207-
disabled={isOnlyNodePool}
217+
disabled={isLkeClusterRestricted || isOnlyNodePool}
208218
onClick={(e) => {
209219
e.stopPropagation();
210220
openDeletePoolDialog(poolId);
@@ -227,6 +237,7 @@ export const NodePool = (props: Props) => {
227237
clusterId={clusterId}
228238
clusterTier={clusterTier}
229239
encryptionStatus={encryptionStatus}
240+
isLkeClusterRestricted={isLkeClusterRestricted}
230241
nodes={nodes}
231242
openRecycleNodeDialog={openRecycleNodeDialog}
232243
poolId={poolId}

packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePoolsDisplay.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const props: Props = {
1313
clusterLabel: 'a cluster',
1414
clusterRegionId: 'us-east',
1515
clusterTier: 'standard',
16+
isLkeClusterRestricted: false,
1617
regionsData: [],
1718
};
1819

packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePoolsDisplay.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface Props {
6666
clusterLabel: string;
6767
clusterRegionId: string;
6868
clusterTier: KubernetesTier;
69+
isLkeClusterRestricted: boolean;
6970
regionsData: Region[];
7071
}
7172

@@ -76,6 +77,7 @@ export const NodePoolsDisplay = (props: Props) => {
7677
clusterLabel,
7778
clusterRegionId,
7879
clusterTier,
80+
isLkeClusterRestricted,
7981
regionsData,
8082
} = props;
8183

@@ -241,10 +243,12 @@ export const NodePoolsDisplay = (props: Props) => {
241243
<ActionMenu
242244
actionsList={[
243245
{
246+
disabled: isLkeClusterRestricted,
244247
onClick: () => setIsRecycleClusterOpen(true),
245248
title: 'Recycle All Nodes',
246249
},
247250
{
251+
disabled: isLkeClusterRestricted,
248252
onClick: handleOpenAddDrawer,
249253
title: 'Add a Node Pool',
250254
},
@@ -256,11 +260,16 @@ export const NodePoolsDisplay = (props: Props) => {
256260
<Hidden mdDown>
257261
<Button
258262
buttonType="outlined"
263+
disabled={isLkeClusterRestricted}
259264
onClick={() => setIsRecycleClusterOpen(true)}
260265
>
261266
Recycle All Nodes
262267
</Button>
263-
<Button buttonType="primary" onClick={handleOpenAddDrawer}>
268+
<Button
269+
buttonType="primary"
270+
disabled={isLkeClusterRestricted}
271+
onClick={handleOpenAddDrawer}
272+
>
264273
Add a Node Pool
265274
</Button>
266275
</Hidden>
@@ -294,6 +303,7 @@ export const NodePoolsDisplay = (props: Props) => {
294303
handleClickAutoscale={handleOpenAutoscaleDrawer}
295304
handleClickLabelsAndTaints={handleOpenLabelsAndTaintsDrawer}
296305
handleClickResize={handleOpenResizeDrawer}
306+
isLkeClusterRestricted={isLkeClusterRestricted}
297307
isOnlyNodePool={pools?.length === 1}
298308
key={id}
299309
nodes={nodes ?? []}

packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface NodeRow {
2626
}
2727

2828
interface NodeRowProps extends NodeRow {
29+
isLkeClusterRestricted: boolean;
2930
linodeError?: APIError[];
3031
openRecycleNodeDialog: (nodeID: string, linodeLabel: string) => void;
3132
typeLabel: string;
@@ -36,6 +37,7 @@ export const NodeRow = React.memo((props: NodeRowProps) => {
3637
instanceId,
3738
instanceStatus,
3839
ip,
40+
isLkeClusterRestricted,
3941
label,
4042
linodeError,
4143
nodeId,
@@ -136,6 +138,7 @@ export const NodeRow = React.memo((props: NodeRowProps) => {
136138
<TableCell>
137139
<NodeActionMenu
138140
instanceLabel={label}
141+
isLkeClusterRestricted={isLkeClusterRestricted}
139142
nodeId={nodeId}
140143
openRecycleNodeDialog={openRecycleNodeDialog}
141144
/>

packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const props: Props = {
3131
clusterId: 1,
3232
clusterTier: 'standard',
3333
encryptionStatus: 'enabled',
34+
isLkeClusterRestricted: false,
3435
nodes: mockKubeNodes,
3536
openRecycleNodeDialog: vi.fn(),
3637
poolId: 1,

packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface Props {
4646
clusterId: number;
4747
clusterTier: KubernetesTier;
4848
encryptionStatus: EncryptionStatus | undefined;
49+
isLkeClusterRestricted: boolean;
4950
nodes: PoolNodeResponse[];
5051
openRecycleNodeDialog: (nodeID: string, linodeLabel: string) => void;
5152
poolId: number;
@@ -65,6 +66,7 @@ export const NodeTable = React.memo((props: Props) => {
6566
encryptionStatus,
6667
nodes,
6768
openRecycleNodeDialog,
69+
isLkeClusterRestricted,
6870
poolId,
6971
regionSupportsDiskEncryption,
7072
statusFilter,
@@ -240,6 +242,7 @@ export const NodeTable = React.memo((props: Props) => {
240242
instanceId={eachRow.instanceId}
241243
instanceStatus={eachRow.instanceStatus}
242244
ip={eachRow.ip}
245+
isLkeClusterRestricted={isLkeClusterRestricted}
243246
key={`node-row-${eachRow.nodeId}`}
244247
label={eachRow.label}
245248
linodeError={error ?? undefined}
@@ -290,7 +293,12 @@ export const NodeTable = React.memo((props: Props) => {
290293
<Typography>Pool ID {poolId}</Typography>
291294
)}
292295
</StyledPoolInfoBox>
293-
<TagCell tags={tags} updateTags={updateTags} view="inline" />
296+
<TagCell
297+
disabled={isLkeClusterRestricted}
298+
tags={tags}
299+
updateTags={updateTags}
300+
view="inline"
301+
/>
294302
</StyledTableFooter>
295303
</>
296304
)}

0 commit comments

Comments
 (0)