Skip to content

Commit

Permalink
use ssm parameter to detect CDK boostrap (#1576)
Browse files Browse the repository at this point in the history
* use ssm parameter to detect CDK boostrap

* use ssm parameter to detect CDK boostrap

* fix that

* use default qualifier
  • Loading branch information
sobolk authored May 31, 2024
1 parent f204baa commit 5b5c15c
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 84 deletions.
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
117 changes: 49 additions & 68 deletions packages/sandbox/src/file_watching_sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ 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_DEFAULT_BOOTSTRAP_VERSION_PARAMETER_NAME,
FileWatchingSandbox,
getBootstrapUrl,
} from './file_watching_sandbox.js';
Expand All @@ -15,7 +14,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 +27,7 @@ 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';

// Watcher mocks
const unsubscribeMockFn = mock.fn();
Expand Down Expand Up @@ -85,24 +84,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: [
{
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',
},
],
},
],
Parameter: {
Name: CDK_DEFAULT_BOOTSTRAP_VERSION_PARAMETER_NAME,
Value: '18',
},
})
);
const openMock = mock.fn(_open, (url: string) => Promise.resolve(url));
Expand Down Expand Up @@ -140,19 +130,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 +154,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 +163,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,21 +172,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: [
{
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',
},
],
},
],
Parameter: {
Name: CDK_DEFAULT_BOOTSTRAP_VERSION_PARAMETER_NAME,
Value: '5',
},
})
);

Expand All @@ -205,7 +186,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 +200,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 +225,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 +237,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
undefined,
false
Expand All @@ -279,7 +260,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 +290,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 +316,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 +345,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 +364,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 +384,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 +404,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 +430,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 +460,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 +505,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 +522,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 +559,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 +602,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 +650,7 @@ void describe('Sandbox using local project name resolver', () => {
({ sandboxInstance } = await setupAndStartSandbox(
{
executor: sandboxExecutor,
cfnClient: cfnClientMock,
ssmClient: ssmClientMock,
},
{
dir: 'testDir',
Expand All @@ -694,7 +675,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 +705,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 +740,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 +785,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 +817,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 +833,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 +847,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 +875,7 @@ const setupAndStartSandbox = async (
type: 'sandbox',
}),
testData.executor,
testData.cfnClient,
testData.ssmClient,
printer as unknown as Printer,
testData.open ?? _open
);
Expand All @@ -909,7 +890,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 +927,6 @@ type SandboxTestData = {
// To instantiate sandbox
sandboxName?: string;
executor: AmplifySandboxExecutor;
cfnClient: CloudFormationClient;
ssmClient: SSMClient;
open?: typeof _open;
};
Loading

0 comments on commit 5b5c15c

Please sign in to comment.