Skip to content

Commit 01207ca

Browse files
fix: Linode Rebuild dialog erroring when Reuse user data previously provide is checked (#11902)
Fixes validation bug affecting the Linode Rebuild flow When trying to rebuild a Linode with the Reuse user data previously provided checkbox checked, a client-side validation error would occur Caused by upcoming: [M3-9535] - Support VPC interfaces on updated Linode Create Networking flow #11847 In that PR, I think I correctly aligned our validation schemas with the API's behavior, but I failed to realize that the changes I made to MetadataSchema would break this flow. Co-authored-by: Banks Nussman <banks@nussman.us>
1 parent dbb40b7 commit 01207ca

4 files changed

Lines changed: 111 additions & 5 deletions

File tree

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/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)