Skip to content

Commit

Permalink
feat: [M3-9167] - Image Encryption Information (#11579)
Browse files Browse the repository at this point in the history
* initial work on landing page

* clean up feature flag

* add changesets

* add new region capability

* Revert "add new region capability"

This reverts commit cec3768.

* mention that images will be automatically encrypted

* new copy

* hide unencrypted icon while image is creating

---------

Co-authored-by: Banks Nussman <[email protected]>
  • Loading branch information
bnussman-akamai and bnussman authored Feb 4, 2025
1 parent 0800df8 commit cc474e0
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 248 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11579-added-1738162685405.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

Visual indication for unencrypted images ([#11579](https://github.com/linode/manager/pull/11579))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Removed `imageServiceGen2` and `imageServiceGen2Ga` feature flags ([#11579](https://github.com/linode/manager/pull/11579))
4 changes: 2 additions & 2 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.
2 changes: 0 additions & 2 deletions packages/manager/src/dev-tools/FeatureFlagTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ const options: { flag: keyof Flags; label: string }[] = [
{ flag: 'blockStorageEncryption', label: 'Block Storage Encryption (BSE)' },
{ flag: 'disableLargestGbPlans', label: 'Disable Largest GB Plans' },
{ flag: 'gecko2', label: 'Gecko' },
{ flag: 'imageServiceGen2', label: 'Image Service Gen2' },
{ flag: 'imageServiceGen2Ga', label: 'Image Service Gen2 GA' },
{ flag: 'limitsEvolution', label: 'Limits Evolution' },
{ flag: 'linodeDiskEncryption', label: 'Linode Disk Encryption (LDE)' },
{ flag: 'linodeInterfaces', label: 'Linode Interfaces' },
Expand Down
2 changes: 0 additions & 2 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,6 @@ export interface Flags {
gecko2: GeckoFeatureFlag;
gpuv2: gpuV2;
iam: BetaFeatureFlag;
imageServiceGen2: boolean;
imageServiceGen2Ga: boolean;
ipv6Sharing: boolean;
limitsEvolution: BaseFeatureFlag;
linodeDiskEncryption: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,42 +149,7 @@ describe('CreateImageTab', () => {
await findByText('Image scheduled for creation.');
});

it('should render a notice if the user selects a Linode in a distributed compute region', async () => {
const region = regionFactory.build({ site_type: 'distributed' });
const linode = linodeFactory.build({ region: region.id });

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

const { findByText, getByLabelText } = await renderWithThemeAndRouter(
<CreateImageTab />
);

const linodeSelect = getByLabelText('Linode');

await userEvent.click(linodeSelect);

const linodeOption = await findByText(linode.label);

await userEvent.click(linodeOption);

// Verify distributed compute region notice renders
await findByText(
"This Linode is in a distributed compute region. These regions can't store images.",
{ exact: false }
);
});

it('should render a notice if the user selects a Linode in a region that does not support image storage and Image Service Gen 2 GA is enabled', async () => {
it('should render a notice if the user selects a Linode in a region that does not support image storage', async () => {
const region = regionFactory.build({ capabilities: [] });
const linode = linodeFactory.build({ region: region.id });

Expand All @@ -201,10 +166,7 @@ describe('CreateImageTab', () => {
);

const { findByText, getByLabelText } = await renderWithThemeAndRouter(
<CreateImageTab />,
{
flags: { imageServiceGen2: true, imageServiceGen2Ga: true },
}
<CreateImageTab />
);

const linodeSelect = getByLabelText('Linode');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,6 @@ export const CreateImageTab = () => {
(r) => r.id === selectedLinode?.region
);

const linodeIsInDistributedRegion =
selectedLinodeRegion?.site_type === 'distributed';

/**
* The 'Object Storage' capability indicates a region can store images
*/
Expand Down Expand Up @@ -211,33 +208,18 @@ export const CreateImageTab = () => {
required
value={selectedLinodeId}
/>
{selectedLinode &&
!linodeRegionSupportsImageStorage &&
flags.imageServiceGen2 &&
flags.imageServiceGen2Ga && (
<Notice variant="warning">
This Linode’s region doesn’t support local image storage. This
image will be stored in the core compute region that’s{' '}
<Link to="https://techdocs.akamai.com/cloud-computing/docs/images#regions-and-captured-custom-images">
geographically closest
</Link>
. After it’s stored, you can replicate it to other{' '}
<Link to="https://www.linode.com/global-infrastructure/">
core compute regions
</Link>
.
</Notice>
)}
{linodeIsInDistributedRegion && !flags.imageServiceGen2Ga && (
{selectedLinode && !linodeRegionSupportsImageStorage && (
<Notice variant="warning">
This Linode is in a distributed compute region. These regions
can't store images. The image is stored in the core compute
region that is{' '}
<Link to="https://www.linode.com/global-infrastructure/">
This Linode’s region doesn’t support local image storage. This
image will be stored in the core compute region that’s{' '}
<Link to="https://techdocs.akamai.com/cloud-computing/docs/images#regions-and-captured-custom-images">
geographically closest
</Link>
. After it's stored, you can replicate it to other core compute
regions.
. After it’s stored, you can replicate it to other{' '}
<Link to="https://www.linode.com/global-infrastructure/">
core compute regions
</Link>
.
</Notice>
)}
<Controller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ describe('Image Table Row', () => {
onRebuild: vi.fn(),
};

it('should render an image row with Image Service Gen2 enabled', async () => {
it('should render an image row with details', async () => {
const image = imageFactory.build({
capabilities: ['cloud-init', 'distributed-sites'],
regions: [
{ region: 'us-east', status: 'available' },
{ region: 'us-southeast', status: 'available' },
Expand All @@ -36,16 +35,13 @@ describe('Image Table Row', () => {
});

const { getByLabelText, getByText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />
)
wrapWithTableBody(<ImageRow handlers={handlers} image={image} />)
);

// Check to see if the row rendered some data
expect(getByText(image.label)).toBeVisible();
expect(getByText(image.id)).toBeVisible();
expect(getByText('Available')).toBeVisible();
expect(getByText('Cloud-init, Distributed')).toBeVisible();
expect(getByText('2 Regions')).toBeVisible();
expect(getByText('0.29 GB')).toBeVisible(); // Size is converted from MB to GB - 300 / 1024 = 0.292
expect(getByText('0.59 GB')).toBeVisible(); // Size is converted from MB to GB - 600 / 1024 = 0.585
Expand All @@ -61,47 +57,63 @@ describe('Image Table Row', () => {
expect(getByText('Delete')).toBeVisible();
});

it('should show a cloud-init icon with a tooltip when Image Service Gen 2 GA is enabled and the image supports cloud-init', () => {
it('should show a cloud-init icon if the image supports it', () => {
const image = imageFactory.build({
capabilities: ['cloud-init'],
regions: [{ region: 'us-east', status: 'available' }],
});

const { getByLabelText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />,
{ flags: { imageServiceGen2: true, imageServiceGen2Ga: true } }
)
wrapWithTableBody(<ImageRow handlers={handlers} image={image} />)
);

expect(
getByLabelText('This image supports our Metadata service via cloud-init.')
).toBeVisible();
});

it('does not show the compatibility column when Image Service Gen2 GA is enabled', () => {
it('should show an unencrypted icon if the image is not "Gen2" (does not have the distributed-site capability)', () => {
const image = imageFactory.build({
capabilities: ['cloud-init', 'distributed-sites'],
capabilities: ['cloud-init'],
regions: [],
status: 'available',
});

const { queryByText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />,
{ flags: { imageServiceGen2: true, imageServiceGen2Ga: true } }
)
const { getByLabelText } = renderWithTheme(
wrapWithTableBody(<ImageRow handlers={handlers} image={image} />)
);

expect(queryByText('Cloud-init, Distributed')).not.toBeInTheDocument();
expect(
getByLabelText('This image is not encrypted.', { exact: false })
).toBeVisible();
});

it('should not show an unencrypted icon when a "Gen2" Image is still "creating"', () => {
// The API does not populate the "distributed-sites" capability until the image is done creating.
// We must account for this because the image would show as "Unencrypted" while it is creating,
// then suddenly show as encrypted once it was done creating. We don't want that.
// Therefore, we decided we won't show the unencrypted icon until the image is done creating to
// prevent confusion.
const image = imageFactory.build({
capabilities: ['cloud-init'],
status: 'creating',
type: 'manual',
});

const { queryByLabelText } = renderWithTheme(
wrapWithTableBody(<ImageRow handlers={handlers} image={image} />)
);

expect(
queryByLabelText('This image is not encrypted.', { exact: false })
).toBeNull();
});

it('should show N/A if multiRegionsEnabled is true, but the Image does not have any regions', () => {
it('should show N/A if Image does not have any regions', () => {
const image = imageFactory.build({ regions: [] });

const { getByText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />,
{ flags: { imageServiceGen2: true } }
)
wrapWithTableBody(<ImageRow handlers={handlers} image={image} />)
);

expect(getByText('N/A')).toBeVisible();
Expand All @@ -113,9 +125,7 @@ describe('Image Table Row', () => {
});

const { getByLabelText, getByText } = renderWithTheme(
wrapWithTableBody(
<ImageRow handlers={handlers} image={image} multiRegionsEnabled />
)
wrapWithTableBody(<ImageRow handlers={handlers} image={image} />)
);

// Open action menu
Expand Down
Loading

0 comments on commit cc474e0

Please sign in to comment.