Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upcoming: [M3-8032] – Add Encrypted/Not Encrypted status to Node Pool table #10480

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 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,13 @@ import Request, {
setURL,
setXFilter,
} from '../request';
import { Filter, Params, ResourcePage as Page } from '../types';
import { CreateImagePayload, Image, ImageUploadPayload, UploadImageResponse } from './types';
import type { Filter, Params, ResourcePage as Page } from '../types';
import type {
CreateImagePayload,
Image,
ImageUploadPayload,
UploadImageResponse,
} from './types';

/**
* Get information about a single Image.
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.
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,3 +1,9 @@
import * as React from 'react';

import Lock from 'src/assets/icons/lock.svg';
import Unlock from 'src/assets/icons/unlock.svg';
import { TooltipIcon } from 'src/components/TooltipIcon';
import { StyledTypography } from 'src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.styles';
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account/account';

Expand Down Expand Up @@ -29,3 +35,18 @@ export const useIsDiskEncryptionFeatureEnabled = (): {

return { isDiskEncryptionFeatureEnabled };
};

export const EncryptedIndicator = (
<>
<Lock />
<StyledTypography>Encrypted</StyledTypography>
</>
);

export const getNotEncryptedIndicatorJSX = (tooltipText: string) => (
<>
<Unlock />
<StyledTypography>Not Encrypted</StyledTypography>
<TooltipIcon status="help" text={tooltipText} />
</>
);
dwiley-akamai marked this conversation as resolved.
Show resolved Hide resolved
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';
mjac0bs marked this conversation as resolved.
Show resolved Hide resolved
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.tsx', async () => {
const actual = await vi.importActual<any>(
'src/components/DiskEncryption/utils.tsx'
);
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,12 @@
import { PoolNodeResponse } from '@linode/api-v4/lib/kubernetes';
import * as React from 'react';

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 {
EncryptedIndicator,
getNotEncryptedIndicatorJSX,
} 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 @@ -16,24 +22,51 @@ 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, 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 ?? []));

const encryptedStatusJSX =
encryptionStatus === 'enabled' ? (
dwiley-akamai marked this conversation as resolved.
Show resolved Hide resolved
dwiley-akamai marked this conversation as resolved.
Show resolved Hide resolved
<>
<StyledVerticalDivider />
{EncryptedIndicator}
</>
) : encryptionStatus === 'disabled' ? (
<>
<StyledVerticalDivider />
{getNotEncryptedIndicatorJSX(DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY)}
</>
) : null;

return (
<OrderBy data={rowData} order={'asc'} orderBy={'label'}>
{({ data: orderedData, handleOrderChange, order, orderBy }) => (
Expand Down Expand Up @@ -116,7 +149,20 @@ 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>
{encryptedStatusJSX}
</Box>
dwiley-akamai marked this conversation as resolved.
Show resolved Hide resolved
) : (
<Typography>Pool ID {poolId}</Typography>
)}
</TableCell>
</TableRow>
</TableFooter>
Expand Down
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
Loading