Skip to content

Commit

Permalink
upcoming: [M3-8032] – Add Encrypted/Not Encrypted status to Node Pool…
Browse files Browse the repository at this point in the history
… table (linode#10480)
  • Loading branch information
dwiley-akamai committed May 23, 2024
1 parent 0d03587 commit 1c5e926
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 23 deletions.
4 changes: 2 additions & 2 deletions packages/api-v4/src/images/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import Request, {
setURL,
setXFilter,
} from '../request';
import { Filter, Params, ResourcePage as Page } from '../types';
import {
import type { Filter, Params, ResourcePage as Page } from '../types';
import type {
CreateImagePayload,
Image,
ImageUploadPayload,
Expand Down
2 changes: 1 addition & 1 deletion packages/api-v4/src/kubernetes/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EncryptionStatus } from 'src/linodes';
import type { EncryptionStatus } from '../linodes';

export interface KubernetesCluster {
created: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add Encrypted/Not Encrypted status to LKE Node Pool table ([#10480](https://github.com/linode/manager/pull/10480))
3 changes: 3 additions & 0 deletions packages/manager/src/assets/icons/divider-vertical.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/manager/src/assets/icons/lock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/manager/src/assets/icons/unlock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/manager/src/components/DiskEncryption/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ export const DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY =

export const DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY =
'Virtual Machine Backups are not encrypted.';

export const DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY =
'To enable disk encryption, delete the node pool and create a new node pool. New node pools are always encrypted.';
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import {
AutoscaleSettings,
PoolNodeResponse,
} from '@linode/api-v4/lib/kubernetes';
import { Theme } from '@mui/material/styles';
import Grid from '@mui/material/Unstable_Grid2';
import * as React from 'react';
Expand All @@ -13,8 +9,15 @@ import { Typography } from 'src/components/Typography';

import { NodeTable } from './NodeTable';

import type {
AutoscaleSettings,
PoolNodeResponse,
} from '@linode/api-v4/lib/kubernetes';
import type { EncryptionStatus } from '@linode/api-v4/lib/linodes/types';

interface Props {
autoscaler: AutoscaleSettings;
encryptionStatus: EncryptionStatus | undefined;
handleClickResize: (poolId: number) => void;
isOnlyNodePool: boolean;
nodes: PoolNodeResponse[];
Expand All @@ -40,9 +43,10 @@ const useStyles = makeStyles()((theme: Theme) => ({
},
}));

const NodePool: React.FC<Props> = (props) => {
export const NodePool = (props: Props) => {
const {
autoscaler,
encryptionStatus,
handleClickResize,
isOnlyNodePool,
nodes,
Expand Down Expand Up @@ -126,6 +130,7 @@ const NodePool: React.FC<Props> = (props) => {
xs={12}
>
<NodeTable
encryptionStatus={encryptionStatus}
nodes={nodes}
openRecycleNodeDialog={openRecycleNodeDialog}
poolId={poolId}
Expand All @@ -135,5 +140,3 @@ const NodePool: React.FC<Props> = (props) => {
</Grid>
);
};

export default NodePool;
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Grid from '@mui/material/Unstable_Grid2';
import { Theme } from '@mui/material/styles';
import { makeStyles } from 'tss-react/mui';
import Grid from '@mui/material/Unstable_Grid2';
import React, { useState } from 'react';
import { Waypoint } from 'react-waypoint';
import { makeStyles } from 'tss-react/mui';

import { Button } from 'src/components/Button/Button';
import { CircleProgress } from 'src/components/CircleProgress';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { Typography } from 'src/components/Typography';
import { Paper } from 'src/components/Paper';
import { Typography } from 'src/components/Typography';
import { useAllKubernetesNodePoolQuery } from 'src/queries/kubernetes';
import { useSpecificTypes } from 'src/queries/types';
import { extendTypesQueryResult } from 'src/utilities/extendType';
Expand All @@ -18,7 +18,7 @@ import { RecycleNodePoolDialog } from '../RecycleNodePoolDialog';
import { AddNodePoolDrawer } from './AddNodePoolDrawer';
import { AutoscalePoolDialog } from './AutoscalePoolDialog';
import { DeleteNodePoolDialog } from './DeleteNodePoolDialog';
import NodePool from './NodePool';
import { NodePool } from './NodePool';
import { RecycleNodeDialog } from './RecycleNodeDialog';
import { ResizeNodePoolDrawer } from './ResizeNodePoolDrawer';

Expand Down Expand Up @@ -152,7 +152,7 @@ export const NodePoolsDisplay = (props: Props) => {
<Grid container direction="column">
<Grid xs={12}>
{_pools?.map((thisPool) => {
const { id, nodes } = thisPool;
const { disk_encryption, id, nodes } = thisPool;

const thisPoolType = types?.find(
(thisType) => thisType.id === thisPool.type
Expand Down Expand Up @@ -181,6 +181,7 @@ export const NodePoolsDisplay = (props: Props) => {
setIsRecycleNodeOpen(true);
}}
autoscaler={thisPool.autoscaler}
encryptionStatus={disk_encryption}
handleClickResize={handleOpenResizeDrawer}
isOnlyNodePool={pools?.length === 1}
nodes={nodes ?? []}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { styled } from '@mui/material/styles';

import VerticalDivider from 'src/assets/icons/divider-vertical.svg';
import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip';
import { Table } from 'src/components/Table';
import { TableRow } from 'src/components/TableRow';
import { Typography } from 'src/components/Typography';

export const StyledTableRow = styled(TableRow, {
label: 'TableRow',
Expand All @@ -19,7 +21,6 @@ export const StyledTableRow = styled(TableRow, {
opacity: 1,
},
marginLeft: 4,
top: 1,
}));

export const StyledTable = styled(Table, {
Expand All @@ -40,3 +41,15 @@ export const StyledCopyTooltip = styled(CopyTooltip, {
marginLeft: 4,
top: 1,
}));

export const StyledVerticalDivider = styled(VerticalDivider, {
label: 'StyledVerticalDivider',
})(({ theme }) => ({
margin: `0 ${theme.spacing(2)}`,
}));

export const StyledTypography = styled(Typography, {
label: 'StyledTypography',
})(({ theme }) => ({
margin: `0 0 0 ${theme.spacing()}`,
}));
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { kubeLinodeFactory } from 'src/factories/kubernetesCluster';
import { linodeFactory } from 'src/factories/linodes';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { NodeTable, Props } from './NodeTable';
import { NodeTable, Props, encryptionStatusTestId } from './NodeTable';

const mockLinodes = linodeFactory.buildList(3);

const mockKubeNodes = kubeLinodeFactory.buildList(3);

const props: Props = {
encryptionStatus: 'enabled',
nodes: mockKubeNodes,
openRecycleNodeDialog: vi.fn(),
poolId: 1,
Expand All @@ -20,6 +21,29 @@ const props: Props = {
beforeAll(() => linodeFactory.resetSequenceNumber());

describe('NodeTable', () => {
const mocks = vi.hoisted(() => {
return {
useIsDiskEncryptionFeatureEnabled: vi.fn(),
};
});

vi.mock('src/components/DiskEncryption/utils.ts', async () => {
const actual = await vi.importActual<any>(
'src/components/DiskEncryption/utils.ts'
);
return {
...actual,
__esModule: true,
useIsDiskEncryptionFeatureEnabled: mocks.useIsDiskEncryptionFeatureEnabled.mockImplementation(
() => {
return {
isDiskEncryptionFeatureEnabled: false, // indicates the feature flag is off or account capability is absent
};
}
),
};
});

it('includes label, status, and IP columns', () => {
const { findByText } = renderWithTheme(<NodeTable {...props} />);
mockLinodes.forEach(async (thisLinode) => {
Expand All @@ -28,8 +52,32 @@ describe('NodeTable', () => {
await findByText('Ready');
});
});

it('includes the Pool ID', () => {
const { getByText } = renderWithTheme(<NodeTable {...props} />);
getByText('Pool ID 1');
});

it('does not display the encryption status of the pool if the account lacks the capability or the feature flag is off', () => {
// situation where isDiskEncryptionFeatureEnabled === false
const { queryByTestId } = renderWithTheme(<NodeTable {...props} />);
const encryptionStatusFragment = queryByTestId(encryptionStatusTestId);

expect(encryptionStatusFragment).not.toBeInTheDocument();
});

it('displays the encryption status of the pool if the feature flag is on and the account has the capability', () => {
mocks.useIsDiskEncryptionFeatureEnabled.mockImplementationOnce(() => {
return {
isDiskEncryptionFeatureEnabled: true,
};
});

const { queryByTestId } = renderWithTheme(<NodeTable {...props} />);
const encryptionStatusFragment = queryByTestId(encryptionStatusTestId);

expect(encryptionStatusFragment).toBeInTheDocument();

mocks.useIsDiskEncryptionFeatureEnabled.mockRestore();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { PoolNodeResponse } from '@linode/api-v4/lib/kubernetes';
import * as React from 'react';

import Lock from 'src/assets/icons/lock.svg';
import Unlock from 'src/assets/icons/unlock.svg';
import { Box } from 'src/components/Box';
import { DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY } from 'src/components/DiskEncryption/constants';
import { useIsDiskEncryptionFeatureEnabled } from 'src/components/DiskEncryption/utils';
import OrderBy from 'src/components/OrderBy';
import Paginate from 'src/components/Paginate';
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
Expand All @@ -11,26 +15,45 @@ import { TableFooter } from 'src/components/TableFooter';
import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
import { TableSortCell } from 'src/components/TableSortCell';
import { TooltipIcon } from 'src/components/TooltipIcon';
import { Typography } from 'src/components/Typography';
import { useAllLinodesQuery } from 'src/queries/linodes/linodes';
import { LinodeWithMaintenance } from 'src/utilities/linodes';

import { NodeRow as _NodeRow } from './NodeRow';
import { StyledTable } from './NodeTable.styles';
import {
StyledTable,
StyledTypography,
StyledVerticalDivider,
} from './NodeTable.styles';

import type { NodeRow } from './NodeRow';
import type { PoolNodeResponse } from '@linode/api-v4/lib/kubernetes';
import type { EncryptionStatus } from '@linode/api-v4/lib/linodes/types';

export interface Props {
encryptionStatus: EncryptionStatus | undefined;
nodes: PoolNodeResponse[];
openRecycleNodeDialog: (nodeID: string, linodeLabel: string) => void;
poolId: number;
typeLabel: string;
}

export const encryptionStatusTestId = 'encryption-status-fragment';

export const NodeTable = React.memo((props: Props) => {
const { nodes, openRecycleNodeDialog, poolId, typeLabel } = props;
const {
encryptionStatus,
nodes,
openRecycleNodeDialog,
poolId,
typeLabel,
} = props;

const { data: linodes, error, isLoading } = useAllLinodesQuery();
const {
isDiskEncryptionFeatureEnabled,
} = useIsDiskEncryptionFeatureEnabled();

const rowData = nodes.map((thisNode) => nodeToRow(thisNode, linodes ?? []));

Expand Down Expand Up @@ -116,7 +139,26 @@ export const NodeTable = React.memo((props: Props) => {
<TableFooter>
<TableRow>
<TableCell colSpan={4}>
<Typography>Pool ID {poolId}</Typography>
{isDiskEncryptionFeatureEnabled &&
encryptionStatus !== undefined ? (
<Box
alignItems="center"
data-testid={encryptionStatusTestId}
display="flex"
flexDirection="row"
>
<Typography>Pool ID {poolId}</Typography>
<StyledVerticalDivider />
<EncryptedStatus
tooltipText={
DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY
}
encryptionStatus={encryptionStatus}
/>
</Box>
) : (
<Typography>Pool ID {poolId}</Typography>
)}
</TableCell>
</TableRow>
</TableFooter>
Expand Down Expand Up @@ -157,3 +199,24 @@ export const nodeToRow = (
nodeStatus: node.status,
};
};

export const EncryptedStatus = ({
encryptionStatus,
tooltipText,
}: {
encryptionStatus: EncryptionStatus;
tooltipText: string | undefined;
}) => {
return encryptionStatus === 'enabled' ? (
<>
<Lock />
<StyledTypography>Encrypted</StyledTypography>
</>
) : encryptionStatus === 'disabled' ? (
<>
<Unlock />
<StyledTypography>Not Encrypted</StyledTypography>
{tooltipText ? <TooltipIcon status="help" text={tooltipText} /> : null}
</>
) : null;
};
9 changes: 7 additions & 2 deletions packages/manager/src/mocks/serverHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,9 +849,14 @@ export const handlers = [
return HttpResponse.json(cluster);
}),
http.get('*/lke/clusters/:clusterId/pools', async () => {
const pools = nodePoolFactory.buildList(10);
const encryptedPools = nodePoolFactory.buildList(5);
const unencryptedPools = nodePoolFactory.buildList(5, {
disk_encryption: 'disabled',
});
nodePoolFactory.resetSequenceNumber();
return HttpResponse.json(makeResourcePage(pools));
return HttpResponse.json(
makeResourcePage([...encryptedPools, ...unencryptedPools])
);
}),
http.get('*/lke/clusters/*/api-endpoints', async () => {
const endpoints = kubeEndpointFactory.buildList(2);
Expand Down

0 comments on commit 1c5e926

Please sign in to comment.