Skip to content

Commit

Permalink
upcoming: [M3-8083] - Linode Create v2 - Disk Encryption (#10535)
Browse files Browse the repository at this point in the history
* add disk encryption to Linode Create v2

* improve testing and add changeset

* use better way to format label

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
  • Loading branch information
bnussman-akamai and bnussman authored Jun 3, 2024
1 parent 894afd5 commit c03915e
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Linode Create v2 - Disk Encryption ([#10535](https://github.com/linode/manager/pull/10535))
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const AccessPanel = (props: Props) => {
disabled={!regionSupportsDiskEncryption}
disabledReason={DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY}
isEncryptDiskChecked={diskEncryptionEnabled ?? false}
toggleDiskEncryptionEnabled={toggleDiskEncryptionEnabled}
onChange={() => toggleDiskEncryptionEnabled()}
/>
</>
) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('DiskEncryption', () => {
<DiskEncryption
descriptionCopy="Description for unit test"
isEncryptDiskChecked={true}
toggleDiskEncryptionEnabled={vi.fn()}
onChange={vi.fn()}
/>
);

Expand All @@ -30,7 +30,7 @@ describe('DiskEncryption', () => {
<DiskEncryption
descriptionCopy="Description for unit test"
isEncryptDiskChecked={true}
toggleDiskEncryptionEnabled={vi.fn()}
onChange={vi.fn()}
/>
);

Expand All @@ -44,7 +44,7 @@ describe('DiskEncryption', () => {
<DiskEncryption
descriptionCopy="Description for unit test"
isEncryptDiskChecked={true}
toggleDiskEncryptionEnabled={vi.fn()}
onChange={vi.fn()}
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import * as React from 'react';
import { Box } from 'src/components/Box';
import { Checkbox } from 'src/components/Checkbox';
import { Typography } from 'src/components/Typography';
import { Notice } from '../Notice/Notice';

export interface DiskEncryptionProps {
descriptionCopy: JSX.Element | string;
disabled?: boolean;
disabledReason?: string;
error?: string;
isEncryptDiskChecked: boolean;
toggleDiskEncryptionEnabled: () => void;
onChange: (checked: boolean) => void;
}

export const headerTestId = 'disk-encryption-header';
Expand All @@ -21,15 +23,19 @@ export const DiskEncryption = (props: DiskEncryptionProps) => {
descriptionCopy,
disabled,
disabledReason,
error,
isEncryptDiskChecked,
toggleDiskEncryptionEnabled,
onChange,
} = props;

return (
<>
<Typography data-testid={headerTestId} variant="h3">
Disk Encryption
</Typography>
{error && (
<Notice spacingBottom={0} spacingTop={8} text={error} variant="error" />
)}
<Typography
data-testid={descriptionTestId}
sx={(theme) => ({ padding: `${theme.spacing()} 0` })}
Expand All @@ -48,7 +54,7 @@ export const DiskEncryption = (props: DiskEncryptionProps) => {
checked={disabled ? false : isEncryptDiskChecked} // in Create flows, this will be defaulted to be checked. Otherwise, we will rely on the current encryption status for the initial value
data-testid={checkboxTestId}
disabled={disabled}
onChange={toggleDiskEncryptionEnabled}
onChange={(e, checked) => onChange(checked)}
text="Encrypt Disk"
toolTipText={disabled ? disabledReason : ''}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { waitFor } from '@testing-library/react';
import React from 'react';

import { profileFactory, sshKeyFactory } from 'src/factories';
import {
accountFactory,
profileFactory,
regionFactory,
sshKeyFactory,
} from 'src/factories';
import { grantsFactory } from 'src/factories/grants';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { HttpResponse, http, server } from 'src/mocks/testServer';
import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';

import { Access } from './Access';

import type { LinodeCreateFormValues } from './utilities';

describe('Access', () => {
it(
'should render a root password input',
Expand Down Expand Up @@ -103,4 +110,53 @@ describe('Access', () => {
expect(getByRole('checkbox')).toBeDisabled();
});
});

it('should show Linode disk encryption if the flag is on and the account has the capability', async () => {
server.use(
http.get('*/v4/account', () => {
return HttpResponse.json(
accountFactory.build({ capabilities: ['Disk Encryption'] })
);
})
);

const { findByText } = renderWithThemeAndHookFormContext({
component: <Access />,
options: { flags: { linodeDiskEncryption: true } },
});

const heading = await findByText('Disk Encryption');

expect(heading).toBeVisible();
expect(heading.tagName).toBe('H3');
});

it('should disable disk encryption if the selected region does not support it', async () => {
const region = regionFactory.build({
capabilities: [],
});

const account = accountFactory.build({ capabilities: ['Disk Encryption'] });

server.use(
http.get('*/v4/account', () => {
return HttpResponse.json(account);
}),
http.get('*/v4/regions', () => {
return HttpResponse.json(makeResourcePage([region]));
})
);

const {
findByLabelText,
} = renderWithThemeAndHookFormContext<LinodeCreateFormValues>({
component: <Access />,
options: { flags: { linodeDiskEncryption: true } },
useFormOptions: { defaultValues: { region: region.id } },
});

await findByLabelText(
'Disk encryption is not available in the selected region.'
);
});
});
43 changes: 42 additions & 1 deletion packages/manager/src/features/Linodes/LinodeCreatev2/Access.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { Controller, useFormContext, useWatch } from 'react-hook-form';

import UserSSHKeyPanel from 'src/components/AccessPanel/UserSSHKeyPanel';
import {
DISK_ENCRYPTION_GENERAL_DESCRIPTION,
DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY,
} from 'src/components/DiskEncryption/constants';
import { DiskEncryption } from 'src/components/DiskEncryption/DiskEncryption';
import { useIsDiskEncryptionFeatureEnabled } from 'src/components/DiskEncryption/utils';
import { Divider } from 'src/components/Divider';
import { Paper } from 'src/components/Paper';
import { Skeleton } from 'src/components/Skeleton';
import { inputMaxWidth } from 'src/foundations/themes/light';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
import { useRegionsQuery } from 'src/queries/regions/regions';

import type { CreateLinodeRequest } from '@linode/api-v4';

Expand All @@ -17,6 +24,19 @@ const PasswordInput = React.lazy(
export const Access = () => {
const { control } = useFormContext<CreateLinodeRequest>();

const {
isDiskEncryptionFeatureEnabled,
} = useIsDiskEncryptionFeatureEnabled();

const { data: regions } = useRegionsQuery();
const regionId = useWatch({ control, name: 'region' });

const selectedRegion = regions?.find((r) => r.id === regionId);

const regionSupportsDiskEncryption = selectedRegion?.capabilities.includes(
'Disk Encryption'
);

const isLinodeCreateRestricted = useRestrictedGlobalGrantCheck({
globalGrantType: 'add_linodes',
});
Expand Down Expand Up @@ -57,6 +77,27 @@ export const Access = () => {
control={control}
name="authorized_users"
/>
{isDiskEncryptionFeatureEnabled && (
<>
<Divider spacingBottom={20} spacingTop={24} />
<Controller
render={({ field, fieldState }) => (
<DiskEncryption
onChange={(checked) =>
field.onChange(checked ? 'enabled' : 'disabled')
}
descriptionCopy={DISK_ENCRYPTION_GENERAL_DESCRIPTION}
disabled={!regionSupportsDiskEncryption}
disabledReason={DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY}
error={fieldState.error?.message}
isEncryptDiskChecked={field.value === 'enabled'}
/>
)}
control={control}
name="disk_encryption"
/>
</>
)}
</Paper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';

import { Backups } from './Backups';

import type { CreateLinodeRequest } from '@linode/api-v4';
import type { LinodeCreateFormValues } from '../utilities';

describe('Linode Create V2 Backups Addon', () => {
it('should render a label and checkbox', () => {
Expand All @@ -30,7 +30,7 @@ describe('Linode Create V2 Backups Addon', () => {
it('should get its value from the form context', () => {
const {
getByRole,
} = renderWithThemeAndHookFormContext<CreateLinodeRequest>({
} = renderWithThemeAndHookFormContext<LinodeCreateFormValues>({
component: <Backups />,
useFormOptions: { defaultValues: { backups_enabled: true } },
});
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('Linode Create V2 Backups Addon', () => {

const {
getByRole,
} = renderWithThemeAndHookFormContext<CreateLinodeRequest>({
} = renderWithThemeAndHookFormContext<LinodeCreateFormValues>({
component: <Backups />,
useFormOptions: { defaultValues: { region: region.id } },
});
Expand All @@ -101,7 +101,7 @@ describe('Linode Create V2 Backups Addon', () => {

const {
getByRole,
} = renderWithThemeAndHookFormContext<CreateLinodeRequest>({
} = renderWithThemeAndHookFormContext<LinodeCreateFormValues>({
component: <Backups />,
});

Expand All @@ -111,4 +111,19 @@ describe('Linode Create V2 Backups Addon', () => {
expect(checkbox).toBeDisabled();
});
});

it('renders a warning if disk encryption is enabled and backups are enabled', async () => {
const {
getByText,
} = renderWithThemeAndHookFormContext<LinodeCreateFormValues>({
component: <Backups />,
useFormOptions: {
defaultValues: { backups_enabled: true, disk_encryption: 'enabled' },
},
});

expect(
getByText('Virtual Machine Backups are not encrypted.')
).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useMemo } from 'react';
import { useController, useWatch } from 'react-hook-form';
import { useController, useFormContext, useWatch } from 'react-hook-form';

import { Checkbox } from 'src/components/Checkbox';
import { Currency } from 'src/components/Currency';
import { DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY } from 'src/components/DiskEncryption/constants';
import { FormControlLabel } from 'src/components/FormControlLabel';
import { Link } from 'src/components/Link';
import { Notice } from 'src/components/Notice/Notice';
import { Stack } from 'src/components/Stack';
import { Typography } from 'src/components/Typography';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
Expand All @@ -15,20 +17,24 @@ import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';

import { getBackupsEnabledValue } from './utilities';

import type { CreateLinodeRequest } from '@linode/api-v4';
import type { LinodeCreateFormValues } from '../utilities';

export const Backups = () => {
const { field } = useController<CreateLinodeRequest, 'backups_enabled'>({
const { control } = useFormContext<LinodeCreateFormValues>();
const { field } = useController({
control,
name: 'backups_enabled',
});

const [regionId, typeId, diskEncryption] = useWatch({
control,
name: ['region', 'type', 'disk_encryption'],
});

const isLinodeCreateRestricted = useRestrictedGlobalGrantCheck({
globalGrantType: 'add_linodes',
});

const regionId = useWatch<CreateLinodeRequest, 'region'>({ name: 'region' });
const typeId = useWatch<CreateLinodeRequest, 'type'>({ name: 'type' });

const { data: type } = useTypeQuery(typeId, Boolean(typeId));
const { data: regions } = useRegionsQuery();
const { data: accountSettings } = useAccountSettings();
Expand All @@ -47,20 +53,21 @@ export const Backups = () => {

const isEdgeRegionSelected = selectedRegion?.site_type === 'edge';

const checked = getBackupsEnabledValue({
accountBackupsEnabled: isAccountBackupsEnabled,
isEdgeRegion: isEdgeRegionSelected,
value: field.value,
});

return (
<FormControlLabel
checked={getBackupsEnabledValue({
accountBackupsEnabled: isAccountBackupsEnabled,
isEdgeRegion: isEdgeRegionSelected,
value: field.value,
})}
disabled={
isEdgeRegionSelected ||
isLinodeCreateRestricted ||
isAccountBackupsEnabled
}
label={
<Stack sx={{ pl: 2 }}>
<Stack spacing={1} sx={{ pl: 2 }}>
<Stack alignItems="center" direction="row" spacing={2}>
<Typography variant="h3">Backups</Typography>
{backupsMonthlyPrice && (
Expand All @@ -69,6 +76,17 @@ export const Backups = () => {
</Typography>
)}
</Stack>
{checked && diskEncryption === 'enabled' && (
<Notice
typeProps={{
style: { fontSize: '0.875rem' },
}}
spacingBottom={0}
spacingTop={0}
text={DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY}
variant="warning"
/>
)}
<Typography>
{isAccountBackupsEnabled ? (
<React.Fragment>
Expand All @@ -86,6 +104,7 @@ export const Backups = () => {
</Typography>
</Stack>
}
checked={checked}
control={<Checkbox />}
onChange={field.onChange}
/>
Expand Down
Loading

0 comments on commit c03915e

Please sign in to comment.