Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use ssm parameter to detect CDK boostrap #1576

Merged
merged 5 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions .changeset/proud-clouds-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/sandbox': patch
---

use ssm parameter to detect CDK boostrap
111 changes: 49 additions & 62 deletions packages/sandbox/src/file_watching_sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import path from 'path';
import watcher from '@parcel/watcher';
import {
CDK_BOOTSTRAP_STACK_NAME,
CDK_BOOTSTRAP_VERSION_KEY,
CDK_BOOTSTRAP_VERSION_PARAMETER_PREFIX,
CDK_BOOTSTRAP_VERSION_PARAMETER_SUFFIX,
FileWatchingSandbox,
getBootstrapUrl,
} from './file_watching_sandbox.js';
Expand All @@ -15,7 +15,6 @@ import {
} from '@aws-amplify/backend-deployer';
import fs from 'fs';
import parseGitIgnore from 'parse-gitignore';
import { CloudFormationClient } from '@aws-sdk/client-cloudformation';
import _open from 'open';
import { SecretListItem, getSecretClient } from '@aws-amplify/backend-secret';
import { Sandbox, SandboxOptions } from './sandbox.js';
Expand All @@ -29,6 +28,8 @@ import {
import { fileURLToPath } from 'url';
import { BackendIdentifier } from '@aws-amplify/plugin-types';
import { AmplifyUserError } from '@aws-amplify/platform-core';
import { SSMClient } from '@aws-sdk/client-ssm';
import { randomUUID } from 'node:crypto';

// Watcher mocks
const unsubscribeMockFn = mock.fn();
Expand Down Expand Up @@ -85,22 +86,15 @@ const backendDeployerDestroyMock = mock.method(backendDeployer, 'destroy', () =>
Promise.resolve()
);
const region = 'test-region';
const cfnClientMock = new CloudFormationClient({ region });
const cfnClientSendMock = mock.fn();
mock.method(cfnClientMock, 'send', cfnClientSendMock);
cfnClientSendMock.mock.mockImplementation(() =>
const ssmClientMock = new SSMClient({ region });
const ssmClientSendMock = mock.fn();
mock.method(ssmClientMock, 'send', ssmClientSendMock);
ssmClientSendMock.mock.mockImplementation(() =>
Promise.resolve({
Stacks: [
Parameters: [
{
Name: CDK_BOOTSTRAP_STACK_NAME,
Outputs: [
{
Description:
'The version of the bootstrap resources that are currently mastered in this stack',
OutputKey: CDK_BOOTSTRAP_VERSION_KEY,
OutputValue: '18',
},
],
Name: `${CDK_BOOTSTRAP_VERSION_PARAMETER_PREFIX}${randomUUID()}${CDK_BOOTSTRAP_VERSION_PARAMETER_SUFFIX}`,
Value: '18',
},
],
})
Expand Down Expand Up @@ -140,19 +134,19 @@ void describe('Sandbox to check if region is bootstrapped', () => {
sandboxInstance = new FileWatchingSandbox(
async () => testSandboxBackendId,
sandboxExecutor,
cfnClientMock,
ssmClientMock,
printer as unknown as Printer,
openMock as never
);

cfnClientSendMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
openMock.mock.resetCalls();
backendDeployerDestroyMock.mock.resetCalls();
backendDeployerDeployMock.mock.resetCalls();
});

afterEach(async () => {
cfnClientSendMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
openMock.mock.resetCalls();
backendDeployerDestroyMock.mock.resetCalls();
backendDeployerDeployMock.mock.resetCalls();
Expand All @@ -164,7 +158,7 @@ void describe('Sandbox to check if region is bootstrapped', () => {
});

void it('when region has not bootstrapped, then opens console to initiate bootstrap', async () => {
cfnClientSendMock.mock.mockImplementationOnce(() => {
ssmClientSendMock.mock.mockImplementationOnce(() => {
throw new Error('Stack with id CDKToolkit does not exist');
});

Expand All @@ -173,7 +167,7 @@ void describe('Sandbox to check if region is bootstrapped', () => {
exclude: ['exclude1', 'exclude2'],
});

assert.strictEqual(cfnClientSendMock.mock.callCount(), 1);
assert.strictEqual(ssmClientSendMock.mock.callCount(), 1);
assert.strictEqual(openMock.mock.callCount(), 1);
assert.strictEqual(
openMock.mock.calls[0].arguments[0],
Expand All @@ -182,19 +176,12 @@ void describe('Sandbox to check if region is bootstrapped', () => {
});

void it('when region has bootstrapped, but with a version lower than the minimum (6), then opens console to initiate bootstrap', async () => {
cfnClientSendMock.mock.mockImplementationOnce(() =>
ssmClientSendMock.mock.mockImplementationOnce(() =>
Promise.resolve({
Stacks: [
Parameters: [
{
Name: CDK_BOOTSTRAP_STACK_NAME,
Outputs: [
{
Description:
'The version of the bootstrap resources that are currently mastered in this stack',
OutputKey: CDK_BOOTSTRAP_VERSION_KEY,
OutputValue: '5',
},
],
Name: `${CDK_BOOTSTRAP_VERSION_PARAMETER_PREFIX}${randomUUID()}${CDK_BOOTSTRAP_VERSION_PARAMETER_SUFFIX}`,
Value: '5',
},
],
})
Expand All @@ -205,7 +192,7 @@ void describe('Sandbox to check if region is bootstrapped', () => {
exclude: ['exclude1', 'exclude2'],
});

assert.strictEqual(cfnClientSendMock.mock.callCount(), 1);
assert.strictEqual(ssmClientSendMock.mock.callCount(), 1);
assert.strictEqual(openMock.mock.callCount(), 1);
assert.strictEqual(
openMock.mock.calls[0].arguments[0],
Expand All @@ -219,7 +206,7 @@ void describe('Sandbox to check if region is bootstrapped', () => {
exclude: ['exclude1', 'exclude2'],
});

assert.strictEqual(cfnClientSendMock.mock.callCount(), 1);
assert.strictEqual(ssmClientSendMock.mock.callCount(), 1);
assert.strictEqual(openMock.mock.callCount(), 0);
});
});
Expand All @@ -244,7 +231,7 @@ void describe('Sandbox using local project name resolver', () => {
backendDeployerDestroyMock.mock.resetCalls();
backendDeployerDeployMock.mock.resetCalls();
subscribeMock.mock.resetCalls();
cfnClientSendMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
await sandboxInstance.stop();

// Printer mocks are reset after the sandbox stop to reset the "Shutting down" call as well.
Expand All @@ -256,7 +243,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
undefined,
false
Expand All @@ -279,7 +266,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{
// imaginary dir does not have any ts files
Expand Down Expand Up @@ -309,7 +296,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{
dir: testDir,
Expand All @@ -335,7 +322,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{
dir: 'testDir',
Expand Down Expand Up @@ -364,13 +351,13 @@ void describe('Sandbox using local project name resolver', () => {
validateAppSources: true,
},
]);
assert.strictEqual(cfnClientSendMock.mock.callCount(), 0);
assert.strictEqual(ssmClientSendMock.mock.callCount(), 0);
});

void it('calls watcher subscribe with the default "./amplify" if no `dir` specified', async () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
await fileChangeEventCallback(null, [
{ type: 'update', path: 'foo/test1.ts' },
Expand All @@ -383,7 +370,7 @@ void describe('Sandbox using local project name resolver', () => {
void it('calls BackendDeployer only once when multiple file changes are present', async () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
await fileChangeEventCallback(null, [
{ type: 'update', path: 'foo/test2.ts' },
Expand All @@ -403,7 +390,7 @@ void describe('Sandbox using local project name resolver', () => {
void it('skips type checking if no typescript change is detected', async () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
await fileChangeEventCallback(null, [
{ type: 'update', path: 'foo/test2.txt' },
Expand All @@ -423,7 +410,7 @@ void describe('Sandbox using local project name resolver', () => {
void it('calls BackendDeployer once when multiple file changes are within few milliseconds (debounce)', async () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
// Not awaiting for this file event to be processed and submitting another one right away
const firstFileChange = fileChangeEventCallback(null, [
Expand All @@ -449,7 +436,7 @@ void describe('Sandbox using local project name resolver', () => {
void it('waits for file changes after completing a deployment and deploys again', async () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
await fileChangeEventCallback(null, [
{ type: 'update', path: 'foo/test5.ts' },
Expand Down Expand Up @@ -479,7 +466,7 @@ void describe('Sandbox using local project name resolver', () => {
void it('queues deployment if a file change is detected during an ongoing deployment', async () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
// Mimic BackendDeployer taking 200 ms.
backendDeployerDeployMock.mock.mockImplementationOnce(async () => {
Expand Down Expand Up @@ -524,7 +511,7 @@ void describe('Sandbox using local project name resolver', () => {
void it('calls BackendDeployer destroy when delete is called', async () => {
({ sandboxInstance } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
await sandboxInstance.delete({});

Expand All @@ -541,7 +528,7 @@ void describe('Sandbox using local project name resolver', () => {
const mockListener = mock.fn();
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
sandboxInstance.on('successfulDeployment', mockListener);
const contextualBackendDeployerMock = contextual.mock.method(
Expand Down Expand Up @@ -578,7 +565,7 @@ void describe('Sandbox using local project name resolver', () => {
void it('handles UpdateNotSupported error while deploying and offers to reset sandbox and customer says yes', async (contextual) => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
const contextualBackendDeployerMock = contextual.mock.method(
backendDeployer,
Expand Down Expand Up @@ -621,7 +608,7 @@ void describe('Sandbox using local project name resolver', () => {
void it('handles UpdateNotSupported error while deploying and offers to reset sandbox and customer says no, continues running sandbox', async (contextual) => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));
const contextualBackendDeployerMock = contextual.mock.method(
backendDeployer,
Expand Down Expand Up @@ -669,7 +656,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{
dir: 'testDir',
Expand All @@ -694,7 +681,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{ identifier: 'customSandboxName' }
));
Expand Down Expand Up @@ -724,7 +711,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{ identifier: 'customSandboxName' }
));
Expand Down Expand Up @@ -759,7 +746,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{
exclude: ['customer_exclude1', 'customer_exclude2'],
Expand Down Expand Up @@ -804,7 +791,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{
exclude: ['customer_exclude1', 'customer_exclude2'],
Expand Down Expand Up @@ -836,7 +823,7 @@ void describe('Sandbox using local project name resolver', () => {
const mockListener = mock.fn();
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));

sandboxInstance.on('successfulDeployment', mockListener);
Expand All @@ -852,7 +839,7 @@ void describe('Sandbox using local project name resolver', () => {
const mockListener = mock.fn();
({ sandboxInstance, fileChangeEventCallback } = await setupAndStartSandbox({
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
}));

sandboxInstance.on('successfulDeletion', mockListener);
Expand All @@ -866,7 +853,7 @@ void describe('Sandbox using local project name resolver', () => {
await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{ watchForChanges: false }
);
Expand Down Expand Up @@ -894,7 +881,7 @@ const setupAndStartSandbox = async (
type: 'sandbox',
}),
testData.executor,
testData.cfnClient,
testData.ssmClient,
printer as unknown as Printer,
testData.open ?? _open
);
Expand All @@ -909,7 +896,7 @@ const setupAndStartSandbox = async (
// Reset all the calls to avoid extra startup call
backendDeployerDestroyMock.mock.resetCalls();
backendDeployerDeployMock.mock.resetCalls();
cfnClientSendMock.mock.resetCalls();
ssmClientSendMock.mock.resetCalls();
listSecretMock.mock.resetCalls();
}

Expand Down Expand Up @@ -946,6 +933,6 @@ type SandboxTestData = {
// To instantiate sandbox
sandboxName?: string;
executor: AmplifySandboxExecutor;
cfnClient: CloudFormationClient;
ssmClient: SSMClient;
open?: typeof _open;
};
Loading
Loading