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
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❀️

});
19 changes: 19 additions & 0 deletions packages/manager/cypress/support/intercepts/linodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<null> => {
return cy.intercept(
'POST',
apiMatcher(`linode/instances/${linodeId}/rebuild`),
makeResponse(linode)
);
};

/**
* Intercepts POST request to rebuild a Linode and mocks an error response.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion packages/validation/src/linodes.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});

Expand Down