From 8e214c985f0e929b852cd1127b2b2d529a447091 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Thu, 20 Mar 2025 19:11:16 -0400 Subject: [PATCH] add fix, add comments, and cypress test --- .../e2e/core/linodes/rebuild-linode.spec.ts | 75 ++++++++++++++++++- .../cypress/support/intercepts/linodes.ts | 19 +++++ .../LinodeRebuild/LinodeRebuildForm.tsx | 17 ++++- packages/validation/src/linodes.schema.ts | 5 +- 4 files changed, 111 insertions(+), 5 deletions(-) diff --git a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts index b472a50bdaa..17bf313023c 100644 --- a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts @@ -1,12 +1,20 @@ import { createStackScript } from '@linode/api-v4/lib'; -import { createLinodeRequestFactory, linodeFactory } from '@src/factories'; +import { + createLinodeRequestFactory, + imageFactory, + linodeFactory, + regionFactory, +} from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; import { interceptRebuildLinode, mockGetLinodeDetails, + mockRebuildLinode, mockRebuildLinodeError, } from 'support/intercepts/linodes'; +import { mockGetRegions } from 'support/intercepts/regions'; import { interceptGetStackScript, interceptGetStackScripts, @@ -340,4 +348,69 @@ describe('rebuild linode', () => { cy.findByText(mockErrorMessage); }); }); + + it('can rebuild a Linode reusing existing user data', () => { + const region = regionFactory.build({ capabilities: ['Metadata'] }); + const linode = linodeFactory.build({ + region: region.id, + // has_user_data: true - add this when we add the type to make this test more realistic + }); + const image = imageFactory.build({ + capabilities: ['cloud-init'], + is_public: true, + }); + + mockRebuildLinode(linode.id, linode).as('rebuildLinode'); + mockGetLinodeDetails(linode.id, linode).as('getLinode'); + mockGetRegions([region]); + mockGetAllImages([image]); + mockGetImage(image.id, image); + + cy.visitWithLogin(`/linodes/${linode.id}?rebuild=true`); + + findRebuildDialog(linode.label).within(() => { + // Select an Image + ui.autocomplete.findByLabel('Image').should('be.visible').click(); + ui.autocompletePopper + .findByTitle(image.label, { exact: false }) + .should('be.visible') + .click(); + + // Type a root password + assertPasswordComplexity(rootPassword, 'Good'); + + // Open the User Data accordion + ui.accordionHeading.findByTitle('Add User Data').scrollIntoView().click(); + + // Verify the reuse checkbox is not checked by default and check it + cy.findByLabelText( + `Reuse user data previously provided for ${linode.label}` + ) + .should('not.be.checked') + .click(); + + // Verify the checkbox becomes checked + cy.findByLabelText( + `Reuse user data previously provided for ${linode.label}` + ).should('be.checked'); + + // Type to confirm + cy.findByLabelText('Linode Label').should('be.visible').click(); + cy.focused().type(linode.label); + + submitRebuild(); + }); + + cy.wait('@rebuildLinode').then((xhr) => { + // Confirm that metadata is NOT in the payload. + // If we omit metadata from the payload, the API will reuse previously provided userdata. + expect(xhr.request.body.metadata).to.be.undefined; + + // Verify other expected values are in the request + expect(xhr.request.body.image).to.equal(image.id); + expect(xhr.request.body.root_pass).to.be.a('string'); + }); + + ui.toast.assertMessage('Linode rebuild started.'); + }); }); diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 2d806f28c6f..b9433352cab 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -189,6 +189,25 @@ export const interceptRebuildLinode = ( ); }; +/** + * Intercepts POST request to rebuild a Linode and mocks the response. + * + * @param linodeId - ID of Linode for intercepted request. + * @param linode - Linode for the mocked response + * + * @returns Cypress chainable. + */ +export const mockRebuildLinode = ( + linodeId: number, + linode: Linode +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linodeId}/rebuild`), + makeResponse(linode) + ); +}; + /** * Intercepts POST request to rebuild a Linode and mocks an error response. * diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/LinodeRebuildForm.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/LinodeRebuildForm.tsx index 8bc0d8cee74..2906b0b1e7c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/LinodeRebuildForm.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/LinodeRebuildForm.tsx @@ -77,10 +77,21 @@ export const LinodeRebuildForm = (props: Props) => { }); const onSubmit = async (values: RebuildLinodeFormValues) => { - if (values.reuseUserData) { - values.metadata = undefined; - } else if (values.metadata?.user_data) { + /** + * User Data logic (see https://github.com/linode/manager/pull/8850) + * 1) if user data has been provided, encode it and include it in the payload + * The backend will use the newly provided user data. + * 2) if the "Reuse User Data" checkbox is checked, remove the Metadata property from the payload + * The backend will continue to use the existing user data. + * 3) if user data has not been provided and the Reuse User Data checkbox is not checked, send null in the payload + * The backend deletes the Linode's user data. The Linode will no longer use user data. + */ + if (values.metadata?.user_data) { values.metadata.user_data = utoa(values.metadata.user_data); + } else if (values.reuseUserData) { + values.metadata = undefined; + } else { + values.metadata = { user_data: null }; } // Distributed instances are encrypted by default and disk_encryption should not be included in the payload. diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts index abfb12d6c65..63a975ea90a 100644 --- a/packages/validation/src/linodes.schema.ts +++ b/packages/validation/src/linodes.schema.ts @@ -390,7 +390,10 @@ export const RebuildLinodeSchema = object({ stackscript_id: number().optional(), stackscript_data: stackscript_data.notRequired(), booted: boolean().optional(), - metadata: MetadataSchema.optional(), + /** + * `metadata` is an optional object with required properties (see https://github.com/jquense/yup/issues/772) + */ + metadata: MetadataSchema.optional().default(undefined), disk_encryption: string().oneOf(['enabled', 'disabled']).optional(), });