Skip to content

Commit 7fc95cb

Browse files
committed
Merge branch 'release-v1.138.0' into staging
2 parents d2c963d + ffaad2e commit 7fc95cb

File tree

6 files changed

+135
-23
lines changed

6 files changed

+135
-23
lines changed

packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { createStackScript } from '@linode/api-v4/lib';
2-
import { createLinodeRequestFactory, linodeFactory } from '@src/factories';
2+
import {
3+
createLinodeRequestFactory,
4+
imageFactory,
5+
linodeFactory,
6+
regionFactory,
7+
} from '@src/factories';
38
import { authenticate } from 'support/api/authentication';
49
import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes';
10+
import { mockGetAllImages, mockGetImage } from 'support/intercepts/images';
511
import {
612
interceptRebuildLinode,
713
mockGetLinodeDetails,
14+
mockRebuildLinode,
815
mockRebuildLinodeError,
916
} from 'support/intercepts/linodes';
17+
import { mockGetRegions } from 'support/intercepts/regions';
1018
import {
1119
interceptGetStackScript,
1220
interceptGetStackScripts,
@@ -340,4 +348,69 @@ describe('rebuild linode', () => {
340348
cy.findByText(mockErrorMessage);
341349
});
342350
});
351+
352+
it('can rebuild a Linode reusing existing user data', () => {
353+
const region = regionFactory.build({ capabilities: ['Metadata'] });
354+
const linode = linodeFactory.build({
355+
region: region.id,
356+
// has_user_data: true - add this when we add the type to make this test more realistic
357+
});
358+
const image = imageFactory.build({
359+
capabilities: ['cloud-init'],
360+
is_public: true,
361+
});
362+
363+
mockRebuildLinode(linode.id, linode).as('rebuildLinode');
364+
mockGetLinodeDetails(linode.id, linode).as('getLinode');
365+
mockGetRegions([region]);
366+
mockGetAllImages([image]);
367+
mockGetImage(image.id, image);
368+
369+
cy.visitWithLogin(`/linodes/${linode.id}?rebuild=true`);
370+
371+
findRebuildDialog(linode.label).within(() => {
372+
// Select an Image
373+
ui.autocomplete.findByLabel('Image').should('be.visible').click();
374+
ui.autocompletePopper
375+
.findByTitle(image.label, { exact: false })
376+
.should('be.visible')
377+
.click();
378+
379+
// Type a root password
380+
assertPasswordComplexity(rootPassword, 'Good');
381+
382+
// Open the User Data accordion
383+
ui.accordionHeading.findByTitle('Add User Data').scrollIntoView().click();
384+
385+
// Verify the reuse checkbox is not checked by default and check it
386+
cy.findByLabelText(
387+
`Reuse user data previously provided for ${linode.label}`
388+
)
389+
.should('not.be.checked')
390+
.click();
391+
392+
// Verify the checkbox becomes checked
393+
cy.findByLabelText(
394+
`Reuse user data previously provided for ${linode.label}`
395+
).should('be.checked');
396+
397+
// Type to confirm
398+
cy.findByLabelText('Linode Label').should('be.visible').click();
399+
cy.focused().type(linode.label);
400+
401+
submitRebuild();
402+
});
403+
404+
cy.wait('@rebuildLinode').then((xhr) => {
405+
// Confirm that metadata is NOT in the payload.
406+
// If we omit metadata from the payload, the API will reuse previously provided userdata.
407+
expect(xhr.request.body.metadata).to.be.undefined;
408+
409+
// Verify other expected values are in the request
410+
expect(xhr.request.body.image).to.equal(image.id);
411+
expect(xhr.request.body.root_pass).to.be.a('string');
412+
});
413+
414+
ui.toast.assertMessage('Linode rebuild started.');
415+
});
343416
});

packages/manager/cypress/support/intercepts/linodes.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,25 @@ export const interceptRebuildLinode = (
189189
);
190190
};
191191

192+
/**
193+
* Intercepts POST request to rebuild a Linode and mocks the response.
194+
*
195+
* @param linodeId - ID of Linode for intercepted request.
196+
* @param linode - Linode for the mocked response
197+
*
198+
* @returns Cypress chainable.
199+
*/
200+
export const mockRebuildLinode = (
201+
linodeId: number,
202+
linode: Linode
203+
): Cypress.Chainable<null> => {
204+
return cy.intercept(
205+
'POST',
206+
apiMatcher(`linode/instances/${linodeId}/rebuild`),
207+
makeResponse(linode)
208+
);
209+
};
210+
192211
/**
193212
* Intercepts POST request to rebuild a Linode and mocks an error response.
194213
*

packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => {
348348
}}
349349
container
350350
direction="column"
351-
spacing={2}
351+
spacing={1}
352352
>
353353
<StyledColumnLabelGrid data-testid="vpc-section-title">
354354
VPC
@@ -358,20 +358,18 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => {
358358
alignItems: 'center',
359359
margin: 0,
360360
padding: '0 0 8px 0',
361-
362361
[theme.breakpoints.down('md')]: {
363362
alignItems: 'start',
364363
display: 'flex',
365364
flexDirection: 'column',
366-
paddingLeft: '8px',
367365
},
368366
}}
369367
container
370368
direction="row"
371-
spacing={2}
369+
spacing={0}
372370
>
373371
<StyledVPCBox>
374-
<StyledListItem>
372+
<StyledListItem sx={{ paddingLeft: 0 }}>
375373
<StyledLabelBox component="span">Label:</StyledLabelBox>{' '}
376374
<Link
377375
data-testid="assigned-vpc-label"
@@ -418,7 +416,7 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => {
418416
1
419417
)} ${theme.spacing(2)}`,
420418
[theme.breakpoints.down('md')]: {
421-
paddingLeft: 3,
419+
paddingLeft: 2,
422420
},
423421
}}
424422
container
@@ -430,6 +428,7 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => {
430428
...(!attachedFirewall && !isLinodeInterfacesEnabled
431429
? { borderRight: 'unset' }
432430
: {}),
431+
paddingLeft: 0,
433432
}}
434433
>
435434
<StyledLabelBox component="span">LKE Cluster:</StyledLabelBox>{' '}
@@ -447,6 +446,7 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => {
447446
<StyledListItem
448447
sx={{
449448
...(!isLinodeInterfacesEnabled ? { borderRight: 'unset' } : {}),
449+
...(!linodeLkeClusterId ? { paddingLeft: 0 } : {}),
450450
}}
451451
>
452452
<StyledLabelBox component="span">Firewall:</StyledLabelBox>{' '}
@@ -461,7 +461,14 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => {
461461
</StyledListItem>
462462
)}
463463
{isLinodeInterfacesEnabled && (
464-
<StyledListItem sx={{ borderRight: 'unset' }}>
464+
<StyledListItem
465+
sx={{
466+
...(!linodeLkeClusterId && !attachedFirewall
467+
? { paddingLeft: 0 }
468+
: {}),
469+
borderRight: 'unset',
470+
}}
471+
>
465472
<StyledLabelBox component="span">Interfaces:</StyledLabelBox>{' '}
466473
{isLinodeInterface ? (
467474
'Linode'

packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { useTheme } from '@mui/material/styles';
1+
import { useLinodeUpdateMutation, useProfile } from '@linode/queries';
22
import Grid from '@mui/material/Grid2';
3+
import { useTheme } from '@mui/material/styles';
34
import { useSnackbar } from 'notistack';
45
import * as React from 'react';
56

67
import { TagCell } from 'src/components/TagCell/TagCell';
78
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
8-
import { useLinodeUpdateMutation, useProfile } from '@linode/queries';
99
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
1010
import { formatDate } from 'src/utilities/formatDate';
1111

@@ -67,16 +67,15 @@ export const LinodeEntityDetailFooter = React.memo((props: FooterProps) => {
6767

6868
return (
6969
<Grid
70-
container
71-
direction="row"
72-
spacing={2}
7370
sx={{
7471
alignItems: 'center',
75-
justifyContent: 'space-between',
7672
flex: 1,
77-
paddingX: 1,
73+
justifyContent: 'space-between',
7874
paddingY: 0,
7975
}}
76+
container
77+
direction="row"
78+
spacing={2}
8079
>
8180
<Grid
8281
size={{
@@ -137,16 +136,16 @@ export const LinodeEntityDetailFooter = React.memo((props: FooterProps) => {
137136
</StyledBox>
138137
</Grid>
139138
<Grid
139+
size={{
140+
lg: 4,
141+
xs: 12,
142+
}}
140143
sx={{
141144
[theme.breakpoints.down('lg')]: {
142145
display: 'flex',
143146
justifyContent: 'flex-start',
144147
},
145148
}}
146-
size={{
147-
lg: 4,
148-
xs: 12,
149-
}}
150149
>
151150
<TagCell
152151
sx={{

packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/LinodeRebuildForm.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,21 @@ export const LinodeRebuildForm = (props: Props) => {
7777
});
7878

7979
const onSubmit = async (values: RebuildLinodeFormValues) => {
80-
if (values.reuseUserData) {
81-
values.metadata = undefined;
82-
} else if (values.metadata?.user_data) {
80+
/**
81+
* User Data logic (see https://github.com/linode/manager/pull/8850)
82+
* 1) if user data has been provided, encode it and include it in the payload
83+
* The backend will use the newly provided user data.
84+
* 2) if the "Reuse User Data" checkbox is checked, remove the Metadata property from the payload
85+
* The backend will continue to use the existing user data.
86+
* 3) if user data has not been provided and the Reuse User Data checkbox is not checked, send null in the payload
87+
* The backend deletes the Linode's user data. The Linode will no longer use user data.
88+
*/
89+
if (values.metadata?.user_data) {
8390
values.metadata.user_data = utoa(values.metadata.user_data);
91+
} else if (values.reuseUserData) {
92+
values.metadata = undefined;
93+
} else {
94+
values.metadata = { user_data: null };
8495
}
8596

8697
// Distributed instances are encrypted by default and disk_encryption should not be included in the payload.

packages/validation/src/linodes.schema.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,10 @@ export const RebuildLinodeSchema = object({
390390
stackscript_id: number().optional(),
391391
stackscript_data: stackscript_data.notRequired(),
392392
booted: boolean().optional(),
393-
metadata: MetadataSchema.optional(),
393+
/**
394+
* `metadata` is an optional object with required properties (see https://github.com/jquense/yup/issues/772)
395+
*/
396+
metadata: MetadataSchema.optional().default(undefined),
394397
disk_encryption: string().oneOf(['enabled', 'disabled']).optional(),
395398
});
396399

0 commit comments

Comments
 (0)