Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions packages/manager/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [2025-02-27] - v1.137.2

### Fixed:

- Disk Encryption logic preventing Linode deployment in distributed regions ([#11760](https://github.com/linode/manager/pull/11760)

## [2025-02-25] - v1.137.1

### Fixed:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { ui } from 'support/ui';
import { accountFactory, regionFactory } from '@src/factories';
import { mockGetRegions } from 'support/intercepts/regions';
import {
linodeFactory,
accountFactory,
linodeTypeFactory,
regionFactory,
} from '@src/factories';
import {
mockGetRegionAvailability,
mockGetRegions,
} from 'support/intercepts/regions';
import { mockGetAccount } from 'support/intercepts/account';
import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags';
import { makeFeatureFlagData } from 'support/util/feature-flags';
import {
checkboxTestId,
headerTestId,
} from 'src/components/Encryption/constants';
import { extendRegion } from 'support/util/regions';
import { linodeCreatePage } from 'support/ui/pages';
import {
mockCreateLinode,
mockGetLinodeTypes,
} from 'support/intercepts/linodes';
import { randomLabel, randomString } from 'support/util/random';
import type { Region } from '@linode/api-v4';

describe('Create Linode with Disk Encryption', () => {
it('should not have a "Disk Encryption" section visible if the feature flag is off and user does not have capability', () => {
Expand Down Expand Up @@ -77,4 +93,123 @@ describe('Create Linode with Disk Encryption', () => {

cy.get(`[data-testid="${checkboxTestId}"]`).should('be.enabled');
});

// Confirm Linode Disk Encryption features when using Distributed Regions.
describe('Distributed regions', () => {
const encryptionTooltipMessage =
'Distributed Compute Instances are encrypted. This setting can not be changed.';

const mockDistributedRegionWithoutCapability = regionFactory.build({
capabilities: [
'Linodes',
'Cloud Firewall',
'Distributed Plans',
'Placement Group',
],
site_type: 'distributed',
});

const mockDistributedRegionWithCapability = regionFactory.build({
capabilities: [
'Linodes',
'Cloud Firewall',
'Distributed Plans',
'Placement Group',
'Disk Encryption',
],
site_type: 'distributed',
});

const mockDistributedRegions: Region[] = [
mockDistributedRegionWithCapability,
mockDistributedRegionWithoutCapability,
];

const mockLinodeType = linodeTypeFactory.build({
id: 'nanode-edge-1',
label: 'Nanode 1GB',
class: 'nanode',
});

/*
* Right now there's some ambiguity over the 'Disk Encryption' capability
* and whether it's expected to be present for Distributed Regions. We'll
* test Cloud against both scenarios -- when distributed regions do and do
* not have the capability -- and confirm that the Linode Create flow works
* as expected in both cases.
*/
mockDistributedRegions.forEach((distributedRegion) => {
const suffix = distributedRegion.capabilities.includes('Disk Encryption')
? '(with region capability)'
: '(without region capability)';

/*
* - Confirms that disk encryption works as expected for distributed regions. Specifically:
* - Encrypted checkbox is always checked, is disabled, and therefore cannot be changed.
* - Outgoing Linode create API request payload does NOT contain encryption property.
*/
it(`creates a Linode with Disk Encryption in a distributed region ${suffix}`, () => {
const mockRegions = [distributedRegion];
const mockLinode = linodeFactory.build({
label: randomLabel(),
region: distributedRegion.id,
});

mockAppendFeatureFlags({
gecko2: {
enabled: true,
},
});

mockGetRegions(mockRegions);
mockGetLinodeTypes([mockLinodeType]);
mockGetRegionAvailability(distributedRegion.id, []);
mockCreateLinode(mockLinode).as('createLinode');
cy.visitWithLogin('/linodes/create');

cy.get('[data-qa-linode-region]').within(() => {
ui.tabList.find().within(() => {
cy.findByText('Distributed').click();
});

cy.findByLabelText('Region').type(distributedRegion.label);
ui.regionSelect
.findItemByRegionLabel(
extendRegion(distributedRegion).label,
mockRegions
)
.click();
});

linodeCreatePage.setLabel(mockLinode.label);
linodeCreatePage.setRootPassword(randomString(32));

// Select mock Nanode plan type.
cy.get('[data-qa-plan-row="Nanode 1 GB"]').click();

cy.findByLabelText('Encrypt Disk')
.should('be.disabled')
.should('be.checked');

cy.findByLabelText(encryptionTooltipMessage).click();
ui.tooltip.findByText(encryptionTooltipMessage).should('be.visible');

// Click "Create Linode" and confirm outgoing API request payload.
ui.button
.findByTitle('Create Linode')
.should('be.visible')
.should('be.enabled')
.click();

// Submit form to create Linode and confirm that outgoing API request
// contains expected user data.
cy.wait('@createLinode').then((xhr) => {
const requestPayload = xhr.request.body;
const regionId = requestPayload['region'];
expect(regionId).to.equal(mockLinode.region);
expect(requestPayload['disk_encryption']).to.be.undefined;
});
});
});
});
});
2 changes: 1 addition & 1 deletion packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "linode-manager",
"author": "Linode",
"description": "The Linode Manager website",
"version": "1.137.1",
"version": "1.137.2",
"private": true,
"type": "module",
"bugs": {
Expand Down
4 changes: 3 additions & 1 deletion packages/manager/src/components/AccessPanel/AccessPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ export const AccessPanel = (props: Props) => {
linodeIsInDistributedRegion,
regionSupportsDiskEncryption,
})}
isEncryptEntityChecked={diskEncryptionEnabled ?? false}
isEncryptEntityChecked={
linodeIsInDistributedRegion || (diskEncryptionEnabled ?? false)
}
onChange={() => toggleDiskEncryptionEnabled()}
/>
</>
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/src/components/Encryption/Encryption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const Encryption = (props: EncryptionProps) => {
flexDirection="row"
>
<Checkbox
checked={disabled ? false : isEncryptEntityChecked} // in Create flows, this will be defaulted to be checked. Otherwise, we will rely on the current encryption status for the initial value
checked={isEncryptEntityChecked}
data-testid={checkboxTestId}
disabled={disabled}
onChange={(e, checked) => onChange(checked)}
Expand Down
23 changes: 14 additions & 9 deletions packages/manager/src/features/Linodes/LinodeCreate/Region.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,19 @@ export const Region = React.memo(() => {
}

if (isDiskEncryptionFeatureEnabled) {
// Enable disk encryption by default if the region supports it
const defaultDiskEncryptionValue = region.capabilities.includes(
'Disk Encryption'
)
? 'enabled'
: undefined;

setValue('disk_encryption', defaultDiskEncryptionValue);
if (region.site_type === 'distributed') {
// If a distributed region is selected, make sure we don't send disk_encryption in the payload.
setValue('disk_encryption', undefined);
} else {
// Enable disk encryption by default if the region supports it
const defaultDiskEncryptionValue = region.capabilities.includes(
'Disk Encryption'
)
? 'enabled'
: undefined;

setValue('disk_encryption', defaultDiskEncryptionValue);
}
}

if (!isLabelFieldDirty) {
Expand Down Expand Up @@ -208,7 +213,7 @@ export const Region = React.memo(() => {
}

return (
<Paper>
<Paper data-qa-linode-region>
<Box display="flex" justifyContent="space-between" mb={1}>
<Typography variant="h2">Region</Typography>
<DocsLink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,10 @@ describe('Security', () => {
expect(heading.tagName).toBe('H3');
});

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

const account = accountFactory.build({ capabilities: ['Disk Encryption'] });
Expand All @@ -158,4 +159,40 @@ describe('Security', () => {
'Disk encryption is not available in the selected region. Select another region to use Disk Encryption.'
);
});

it('should disable the disk encryption checkbox (but show it as enabled) if the selected region is a distributed region', async () => {
const region = regionFactory.build({
capabilities: ['Disk Encryption'],
site_type: 'distributed',
});

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,
getByLabelText,
} = renderWithThemeAndHookFormContext<LinodeCreateFormValues>({
component: <Security />,
options: { flags: { linodeDiskEncryption: true } },
useFormOptions: { defaultValues: { region: region.id } },
});

await findByLabelText(
'Distributed Compute Instances are encrypted. This setting can not be changed.'
);

const checkbox = getByLabelText('Encrypt Disk');

expect(checkbox).toBeChecked();
expect(checkbox).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,14 @@ export const Security = () => {
? DISK_ENCRYPTION_DEFAULT_DISTRIBUTED_INSTANCES
: DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY
}
isEncryptEntityChecked={
isDistributedRegion || field.value === 'enabled'
}
onChange={(checked) =>
field.onChange(checked ? 'enabled' : 'disabled')
}
disabled={!regionSupportsDiskEncryption}
disabled={isDistributedRegion || !regionSupportsDiskEncryption}
error={fieldState.error?.message}
isEncryptEntityChecked={field.value === 'enabled'}
/>
)}
control={control}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,21 @@ describe('Linode Create Summary', () => {

await findByText(`5 Nodes - $10/month $2.50/hr`);
});

it('should render "Encrypted" if a distributed region is selected', async () => {
const region = regionFactory.build({ site_type: 'distributed' });

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

const { findByText } = renderWithThemeAndHookFormContext({
component: <Summary />,
useFormOptions: { defaultValues: { region: region.id } },
});

await findByText('Encrypted');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export const Summary = () => {
item: {
title: 'Encrypted',
},
show: diskEncryption === 'enabled',
show: diskEncryption === 'enabled' || region?.site_type === 'distributed',
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const TwoStepRegion = (props: CombinedProps) => {
const { params } = useLinodeCreateQueryParams();

return (
<Paper data-testid="region">
<Paper data-testid="region" data-qa-linode-region>
<Box display="flex" justifyContent="space-between" mb={1}>
<Typography variant="h2">Region</Typography>
<DocsLink
Expand Down