aws-cdk
Version:
CDK Toolkit, the command line tool for CDK apps
1,081 lines • 157 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// We need to mock the chokidar library, used by 'cdk watch'
const mockChokidarWatcherOn = jest.fn();
const fakeChokidarWatcher = {
on: mockChokidarWatcherOn,
};
const fakeChokidarWatcherOn = {
get readyCallback() {
expect(mockChokidarWatcherOn.mock.calls.length).toBeGreaterThanOrEqual(1);
// The call to the first 'watcher.on()' in the production code is the one we actually want here.
// This is a pretty fragile, but at least with this helper class,
// we would have to change it only in one place if it ever breaks
const firstCall = mockChokidarWatcherOn.mock.calls[0];
// let's make sure the first argument is the 'ready' event,
// just to be double safe
expect(firstCall[0]).toBe('ready');
// the second argument is the callback
return firstCall[1];
},
get fileEventCallback() {
expect(mockChokidarWatcherOn.mock.calls.length).toBeGreaterThanOrEqual(2);
const secondCall = mockChokidarWatcherOn.mock.calls[1];
// let's make sure the first argument is not the 'ready' event,
// just to be double safe
expect(secondCall[0]).not.toBe('ready');
// the second argument is the callback
return secondCall[1];
},
};
const mockChokidarWatch = jest.fn();
jest.mock('chokidar', () => ({
watch: mockChokidarWatch,
}));
const fakeChokidarWatch = {
get includeArgs() {
expect(mockChokidarWatch.mock.calls.length).toBe(1);
// the include args are the first parameter to the 'watch()' call
return mockChokidarWatch.mock.calls[0][0];
},
get excludeArgs() {
expect(mockChokidarWatch.mock.calls.length).toBe(1);
// the ignore args are a property of the second parameter to the 'watch()' call
const chokidarWatchOpts = mockChokidarWatch.mock.calls[0][1];
return chokidarWatchOpts.ignored;
},
};
const mockData = jest.fn();
jest.mock('../lib/logging', () => ({
...jest.requireActual('../lib/logging'),
data: mockData,
}));
jest.setTimeout(30000);
const os = require("os");
const path = require("path");
const cxschema = require("@aws-cdk/cloud-assembly-schema");
const cloud_assembly_schema_1 = require("@aws-cdk/cloud-assembly-schema");
const cxapi = require("@aws-cdk/cx-api");
const fs = require("fs-extra");
const util_1 = require("./util");
const mock_sdk_1 = require("./util/mock-sdk");
const bootstrap_1 = require("../lib/api/bootstrap");
const deployments_1 = require("../lib/api/deployments");
const common_1 = require("../lib/api/hotswap/common");
const cdk_toolkit_1 = require("../lib/cdk-toolkit");
const diff_1 = require("../lib/diff");
const util_2 = require("../lib/util");
let cloudExecutable;
let bootstrapper;
let stderrMock;
beforeEach(() => {
jest.resetAllMocks();
mockChokidarWatch.mockReturnValue(fakeChokidarWatcher);
// on() in chokidar's Watcher returns 'this'
mockChokidarWatcherOn.mockReturnValue(fakeChokidarWatcher);
bootstrapper = (0, util_1.instanceMockFrom)(bootstrap_1.Bootstrapper);
bootstrapper.bootstrapEnvironment.mockResolvedValue({ noOp: false, outputs: {} });
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
MockStack.MOCK_STACK_A,
MockStack.MOCK_STACK_B,
],
nestedAssemblies: [{
stacks: [MockStack.MOCK_STACK_C],
}],
});
stderrMock = jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; });
});
function defaultToolkitSetup() {
return new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
deployments: new FakeCloudFormation({
'Test-Stack-A': { Foo: 'Bar' },
'Test-Stack-B': { Baz: 'Zinga!' },
'Test-Stack-C': { Baz: 'Zinga!' },
}),
});
}
describe('readCurrentTemplate', () => {
let template;
let mockForEnvironment = jest.fn();
let mockCloudExecutable;
beforeEach(() => {
template = {
Resources: {
Func: {
Type: 'AWS::Lambda::Function',
Properties: {
Key: 'Value',
},
},
},
};
mockCloudExecutable = new util_1.MockCloudExecutable({
stacks: [
{
stackName: 'Test-Stack-C',
template,
properties: {
assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}',
lookupRole: {
arn: 'bloop-lookup:${AWS::Region}:${AWS::AccountId}',
requiresBootstrapStackVersion: 5,
bootstrapStackVersionSsmParameter: '/bootstrap/parameter',
},
},
},
{
stackName: 'Test-Stack-A',
template,
properties: {
assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}',
},
},
],
});
mockForEnvironment = jest.fn().mockImplementation(() => {
return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true };
});
mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment;
mockCloudExecutable.sdkProvider.stubCloudFormation({
getTemplate() {
return {
TemplateBody: JSON.stringify(template),
};
},
describeStacks() {
return {
Stacks: [
{
StackName: 'Test-Stack-C',
StackStatus: 'CREATE_COMPLETE',
CreationTime: new Date(),
},
{
StackName: 'Test-Stack-A',
StackStatus: 'CREATE_COMPLETE',
CreationTime: new Date(),
},
],
};
},
});
});
test('lookup role is used', async () => {
// GIVEN
let requestedParameterName;
mockCloudExecutable.sdkProvider.stubSSM({
getParameter(request) {
requestedParameterName = request.Name;
return {
Parameter: {
Value: '6',
},
};
},
});
const cdkToolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable: mockCloudExecutable,
configuration: mockCloudExecutable.configuration,
sdkProvider: mockCloudExecutable.sdkProvider,
deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }),
});
// WHEN
await cdkToolkit.deploy({
selector: { patterns: ['Test-Stack-C'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
// THEN
expect(requestedParameterName).toEqual('/bootstrap/parameter');
expect(mockForEnvironment.mock.calls.length).toEqual(2);
expect(mockForEnvironment.mock.calls[0][2]).toEqual({
assumeRoleArn: 'bloop-lookup:here:123456789012',
});
});
test('fallback to deploy role if bootstrap stack version is not valid', async () => {
// GIVEN
let requestedParameterName;
mockCloudExecutable.sdkProvider.stubSSM({
getParameter(request) {
requestedParameterName = request.Name;
return {
Parameter: {
Value: '1',
},
};
},
});
const cdkToolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable: mockCloudExecutable,
configuration: mockCloudExecutable.configuration,
sdkProvider: mockCloudExecutable.sdkProvider,
deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }),
});
// WHEN
await cdkToolkit.deploy({
selector: { patterns: ['Test-Stack-C'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
// THEN
expect((0, util_2.flatten)(stderrMock.mock.calls)).toEqual(expect.arrayContaining([
expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/),
expect.stringMatching(/please upgrade to bootstrap version >= 5/),
]));
expect(requestedParameterName).toEqual('/bootstrap/parameter');
expect(mockForEnvironment.mock.calls.length).toEqual(3);
expect(mockForEnvironment.mock.calls[0][2]).toEqual({
assumeRoleArn: 'bloop-lookup:here:123456789012',
});
expect(mockForEnvironment.mock.calls[1][2]).toEqual({
assumeRoleArn: 'bloop:here:123456789012',
});
});
test('fallback to deploy role if bootstrap version parameter not found', async () => {
// GIVEN
mockCloudExecutable.sdkProvider.stubSSM({
getParameter() {
throw new Error('not found');
},
});
const cdkToolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable: mockCloudExecutable,
configuration: mockCloudExecutable.configuration,
sdkProvider: mockCloudExecutable.sdkProvider,
deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }),
});
// WHEN
await cdkToolkit.deploy({
selector: { patterns: ['Test-Stack-C'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
// THEN
expect((0, util_2.flatten)(stderrMock.mock.calls)).toEqual(expect.arrayContaining([
expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/),
expect.stringMatching(/please upgrade to bootstrap version >= 5/),
]));
expect(mockForEnvironment.mock.calls.length).toEqual(3);
expect(mockForEnvironment.mock.calls[0][2]).toEqual({
assumeRoleArn: 'bloop-lookup:here:123456789012',
});
expect(mockForEnvironment.mock.calls[1][2]).toEqual({
assumeRoleArn: 'bloop:here:123456789012',
});
});
test('fallback to deploy role if forEnvironment throws', async () => {
// GIVEN
// throw error first for the 'prepareSdkWithLookupRoleFor' call and succeed for the rest
mockForEnvironment = jest.fn().mockImplementationOnce(() => { throw new Error('error'); })
.mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; });
mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment;
mockCloudExecutable.sdkProvider.stubSSM({
getParameter() {
return {};
},
});
const cdkToolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable: mockCloudExecutable,
configuration: mockCloudExecutable.configuration,
sdkProvider: mockCloudExecutable.sdkProvider,
deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }),
});
// WHEN
await cdkToolkit.deploy({
selector: { patterns: ['Test-Stack-C'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
// THEN
expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled();
expect((0, util_2.flatten)(stderrMock.mock.calls)).toEqual(expect.arrayContaining([
expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/),
expect.stringMatching(/please upgrade to bootstrap version >= 5/),
]));
expect(mockForEnvironment.mock.calls.length).toEqual(3);
expect(mockForEnvironment.mock.calls[0][2]).toEqual({
assumeRoleArn: 'bloop-lookup:here:123456789012',
});
expect(mockForEnvironment.mock.calls[1][2]).toEqual({
assumeRoleArn: 'bloop:here:123456789012',
});
});
test('dont lookup bootstrap version parameter if default credentials are used', async () => {
// GIVEN
mockForEnvironment = jest.fn().mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: false }; });
mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment;
const cdkToolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable: mockCloudExecutable,
configuration: mockCloudExecutable.configuration,
sdkProvider: mockCloudExecutable.sdkProvider,
deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }),
});
mockCloudExecutable.sdkProvider.stubSSM({
getParameter() {
return {};
},
});
// WHEN
await cdkToolkit.deploy({
selector: { patterns: ['Test-Stack-C'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
// THEN
expect((0, util_2.flatten)(stderrMock.mock.calls)).toEqual(expect.arrayContaining([
expect.stringMatching(/please upgrade to bootstrap version >= 5/),
]));
expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled();
expect(mockForEnvironment.mock.calls.length).toEqual(3);
expect(mockForEnvironment.mock.calls[0][2]).toEqual({
assumeRoleArn: 'bloop-lookup:here:123456789012',
});
expect(mockForEnvironment.mock.calls[1][2]).toEqual({
assumeRoleArn: 'bloop:here:123456789012',
});
});
test('do not print warnings if lookup role not provided in stack artifact', async () => {
// GIVEN
mockCloudExecutable.sdkProvider.stubSSM({
getParameter() {
return {};
},
});
const cdkToolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable: mockCloudExecutable,
configuration: mockCloudExecutable.configuration,
sdkProvider: mockCloudExecutable.sdkProvider,
deployments: new deployments_1.Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }),
});
// WHEN
await cdkToolkit.deploy({
selector: { patterns: ['Test-Stack-A'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
// THEN
expect((0, util_2.flatten)(stderrMock.mock.calls)).not.toEqual(expect.arrayContaining([
expect.stringMatching(/Could not assume/),
expect.stringMatching(/please upgrade to bootstrap version/),
]));
expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled();
expect(mockForEnvironment.mock.calls.length).toEqual(2);
expect(mockForEnvironment.mock.calls[0][2]).toEqual({
assumeRoleArn: undefined,
assumeRoleExternalId: undefined,
});
});
});
describe('deploy', () => {
test('fails when no valid stack names are given', async () => {
// GIVEN
const toolkit = defaultToolkitSetup();
// WHEN
await expect(() => toolkit.deploy({
selector: { patterns: ['Test-Stack-D'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
})).rejects.toThrow('No stacks match the name(s) Test-Stack-D');
});
describe('with hotswap deployment', () => {
test("passes through the 'hotswap' option to CloudFormationDeployments.deployStack()", async () => {
// GIVEN
const mockCfnDeployments = (0, util_1.instanceMockFrom)(deployments_1.Deployments);
mockCfnDeployments.deployStack.mockReturnValue(Promise.resolve({
noOp: false,
outputs: {},
stackArn: 'stackArn',
stackArtifact: (0, util_1.instanceMockFrom)(cxapi.CloudFormationStackArtifact),
}));
const cdkToolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
deployments: mockCfnDeployments,
});
// WHEN
await cdkToolkit.deploy({
selector: { patterns: ['Test-Stack-A-Display-Name'] },
requireApproval: diff_1.RequireApproval.Never,
hotswap: common_1.HotswapMode.FALL_BACK,
});
// THEN
expect(mockCfnDeployments.deployStack).toHaveBeenCalledWith(expect.objectContaining({
hotswap: common_1.HotswapMode.FALL_BACK,
}));
});
});
describe('makes correct CloudFormation calls', () => {
test('without options', async () => {
// GIVEN
const toolkit = defaultToolkitSetup();
// WHEN
await toolkit.deploy({
selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
});
test('with stacks all stacks specified as double wildcard', async () => {
// GIVEN
const toolkit = defaultToolkitSetup();
// WHEN
await toolkit.deploy({
selector: { patterns: ['**'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
});
test('with one stack specified', async () => {
// GIVEN
const toolkit = defaultToolkitSetup();
// WHEN
await toolkit.deploy({
selector: { patterns: ['Test-Stack-A-Display-Name'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
});
test('with stacks all stacks specified as wildcard', async () => {
// GIVEN
const toolkit = defaultToolkitSetup();
// WHEN
await toolkit.deploy({
selector: { patterns: ['*'] },
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
});
test('with sns notification arns', async () => {
// GIVEN
const notificationArns = [
'arn:aws:sns:us-east-2:444455556666:MyTopic',
'arn:aws:sns:eu-west-1:111155556666:my-great-topic',
];
const toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
deployments: new FakeCloudFormation({
'Test-Stack-A': { Foo: 'Bar' },
'Test-Stack-B': { Baz: 'Zinga!' },
}, notificationArns),
});
// WHEN
await toolkit.deploy({
selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] },
notificationArns,
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
});
});
test('fail with incorrect sns notification arns', async () => {
// GIVEN
const notificationArns = ['arn:::cfn-my-cool-topic'];
const toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
deployments: new FakeCloudFormation({
'Test-Stack-A': { Foo: 'Bar' },
}, notificationArns),
});
// WHEN
await expect(() => toolkit.deploy({
selector: { patterns: ['Test-Stack-A'] },
notificationArns,
hotswap: common_1.HotswapMode.FULL_DEPLOYMENT,
})).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic');
});
test('globless bootstrap uses environment without question', async () => {
// GIVEN
const toolkit = defaultToolkitSetup();
// WHEN
await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {});
// THEN
expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({
account: '56789',
region: 'south-pole',
name: 'aws://56789/south-pole',
}, expect.anything(), expect.anything());
expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1);
});
test('globby bootstrap uses whats in the stacks', async () => {
// GIVEN
const toolkit = defaultToolkitSetup();
cloudExecutable.configuration.settings.set(['app'], 'something');
// WHEN
await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {});
// THEN
expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({
account: '123456789012',
region: 'bermuda-triangle-1',
name: 'aws://123456789012/bermuda-triangle-1',
}, expect.anything(), expect.anything());
expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1);
});
test('bootstrap can be invoked without the --app argument', async () => {
// GIVEN
cloudExecutable.configuration.settings.clear();
const mockSynthesize = jest.fn();
cloudExecutable.synthesize = mockSynthesize;
const toolkit = defaultToolkitSetup();
// WHEN
await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {});
// THEN
expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({
account: '123456789012',
region: 'west-pole',
name: 'aws://123456789012/west-pole',
}, expect.anything(), expect.anything());
expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1);
expect(cloudExecutable.hasApp).toEqual(false);
expect(mockSynthesize).not.toHaveBeenCalled();
});
});
});
describe('destroy', () => {
test('destroy correct stack', async () => {
const toolkit = defaultToolkitSetup();
await expect(() => {
return toolkit.destroy({
selector: { patterns: ['Test-Stack-A/Test-Stack-C'] },
exclusively: true,
force: true,
fromDeploy: true,
});
}).resolves;
});
});
describe('watch', () => {
test("fails when no 'watch' settings are found", async () => {
const toolkit = defaultToolkitSetup();
await expect(() => {
return toolkit.watch({
selector: { patterns: [] },
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
}).rejects.toThrow("Cannot use the 'watch' command without specifying at least one directory to monitor. " +
'Make sure to add a "watch" key to your cdk.json');
});
test('observes only the root directory by default', async () => {
cloudExecutable.configuration.settings.set(['watch'], {});
const toolkit = defaultToolkitSetup();
await toolkit.watch({
selector: { patterns: [] },
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
const includeArgs = fakeChokidarWatch.includeArgs;
expect(includeArgs.length).toBe(1);
});
test("allows providing a single string in 'watch.include'", async () => {
cloudExecutable.configuration.settings.set(['watch'], {
include: 'my-dir',
});
const toolkit = defaultToolkitSetup();
await toolkit.watch({
selector: { patterns: [] },
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir']);
});
test("allows providing an array of strings in 'watch.include'", async () => {
cloudExecutable.configuration.settings.set(['watch'], {
include: ['my-dir1', '**/my-dir2/*'],
});
const toolkit = defaultToolkitSetup();
await toolkit.watch({
selector: { patterns: [] },
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir1', '**/my-dir2/*']);
});
test('ignores the output dir, dot files, dot directories, and node_modules by default', async () => {
cloudExecutable.configuration.settings.set(['watch'], {});
cloudExecutable.configuration.settings.set(['output'], 'cdk.out');
const toolkit = defaultToolkitSetup();
await toolkit.watch({
selector: { patterns: [] },
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
expect(fakeChokidarWatch.excludeArgs).toStrictEqual([
'cdk.out/**',
'**/.*',
'**/.*/**',
'**/node_modules/**',
]);
});
test("allows providing a single string in 'watch.exclude'", async () => {
cloudExecutable.configuration.settings.set(['watch'], {
exclude: 'my-dir',
});
const toolkit = defaultToolkitSetup();
await toolkit.watch({
selector: { patterns: [] },
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
const excludeArgs = fakeChokidarWatch.excludeArgs;
expect(excludeArgs.length).toBe(5);
expect(excludeArgs[0]).toBe('my-dir');
});
test("allows providing an array of strings in 'watch.exclude'", async () => {
cloudExecutable.configuration.settings.set(['watch'], {
exclude: ['my-dir1', '**/my-dir2'],
});
const toolkit = defaultToolkitSetup();
await toolkit.watch({
selector: { patterns: [] },
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
const excludeArgs = fakeChokidarWatch.excludeArgs;
expect(excludeArgs.length).toBe(6);
expect(excludeArgs[0]).toBe('my-dir1');
expect(excludeArgs[1]).toBe('**/my-dir2');
});
test('allows watching with deploy concurrency', async () => {
cloudExecutable.configuration.settings.set(['watch'], {});
const toolkit = defaultToolkitSetup();
const cdkDeployMock = jest.fn();
toolkit.deploy = cdkDeployMock;
await toolkit.watch({
selector: { patterns: [] },
concurrency: 3,
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
fakeChokidarWatcherOn.readyCallback();
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 }));
});
describe.each([common_1.HotswapMode.FALL_BACK, common_1.HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => {
test('passes through the correct hotswap mode to deployStack()', async () => {
cloudExecutable.configuration.settings.set(['watch'], {});
const toolkit = defaultToolkitSetup();
const cdkDeployMock = jest.fn();
toolkit.deploy = cdkDeployMock;
await toolkit.watch({ selector: { patterns: [] }, hotswap: hotswapMode });
fakeChokidarWatcherOn.readyCallback();
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode }));
});
});
test('respects HotswapMode.HOTSWAP_ONLY', async () => {
cloudExecutable.configuration.settings.set(['watch'], {});
const toolkit = defaultToolkitSetup();
const cdkDeployMock = jest.fn();
toolkit.deploy = cdkDeployMock;
await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.HOTSWAP_ONLY });
fakeChokidarWatcherOn.readyCallback();
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: common_1.HotswapMode.HOTSWAP_ONLY }));
});
test('respects HotswapMode.FALL_BACK', async () => {
cloudExecutable.configuration.settings.set(['watch'], {});
const toolkit = defaultToolkitSetup();
const cdkDeployMock = jest.fn();
toolkit.deploy = cdkDeployMock;
await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.FALL_BACK });
fakeChokidarWatcherOn.readyCallback();
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: common_1.HotswapMode.FALL_BACK }));
});
test('respects HotswapMode.FULL_DEPLOYMENT', async () => {
cloudExecutable.configuration.settings.set(['watch'], {});
const toolkit = defaultToolkitSetup();
const cdkDeployMock = jest.fn();
toolkit.deploy = cdkDeployMock;
await toolkit.watch({ selector: { patterns: [] }, hotswap: common_1.HotswapMode.FULL_DEPLOYMENT });
fakeChokidarWatcherOn.readyCallback();
expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: common_1.HotswapMode.FULL_DEPLOYMENT }));
});
describe('with file change events', () => {
let toolkit;
let cdkDeployMock;
beforeEach(async () => {
cloudExecutable.configuration.settings.set(['watch'], {});
toolkit = defaultToolkitSetup();
cdkDeployMock = jest.fn();
toolkit.deploy = cdkDeployMock;
await toolkit.watch({
selector: { patterns: [] },
hotswap: common_1.HotswapMode.HOTSWAP_ONLY,
});
});
test("does not trigger a 'deploy' before the 'ready' event has fired", async () => {
await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file');
expect(cdkDeployMock).not.toHaveBeenCalled();
});
describe("when the 'ready' event has already fired", () => {
beforeEach(() => {
// The ready callback triggers a deployment so each test
// that uses this function will see 'cdkDeployMock' called
// an additional time.
fakeChokidarWatcherOn.readyCallback();
});
test("an initial 'deploy' is triggered, without any file changes", async () => {
expect(cdkDeployMock).toHaveBeenCalledTimes(1);
});
test("does trigger a 'deploy' for a file change", async () => {
await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file');
expect(cdkDeployMock).toHaveBeenCalledTimes(2);
});
test("triggers a 'deploy' twice for two file changes", async () => {
await Promise.all([
fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'),
fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'),
]);
expect(cdkDeployMock).toHaveBeenCalledTimes(3);
});
test("batches file changes that happen during 'deploy'", async () => {
await Promise.all([
fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'),
fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'),
fakeChokidarWatcherOn.fileEventCallback('unlink', 'my-file3'),
fakeChokidarWatcherOn.fileEventCallback('add', 'my-file4'),
]);
expect(cdkDeployMock).toHaveBeenCalledTimes(3);
});
});
});
});
describe('synth', () => {
test('successful synth outputs hierarchical stack ids', async () => {
const toolkit = defaultToolkitSetup();
await toolkit.synth([], false, false);
// Separate tests as colorizing hampers detection
expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-A-Display-Name');
expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-B');
});
test('with no stdout option', async () => {
// GIVE
const toolkit = defaultToolkitSetup();
// THEN
await toolkit.synth(['Test-Stack-A-Display-Name'], false, true);
expect(mockData.mock.calls.length).toEqual(0);
});
afterEach(() => {
process.env.STACKS_TO_VALIDATE = undefined;
});
describe('migrate', () => {
const testResourcePath = [__dirname, 'commands', 'test-resources'];
const templatePath = [...testResourcePath, 'templates'];
const sqsTemplatePath = path.join(...templatePath, 'sqs-template.json');
const autoscalingTemplatePath = path.join(...templatePath, 'autoscaling-template.yml');
const s3TemplatePath = path.join(...templatePath, 's3-template.json');
test('migrate fails when neither --from-path or --from-stack are provided', async () => {
const toolkit = defaultToolkitSetup();
await expect(() => toolkit.migrate({ stackName: 'no-source' })).rejects.toThrowError('Either `--from-path` or `--from-stack` must be used to provide the source of the CloudFormation template.');
expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Either `--from-path` or `--from-stack` must be used to provide the source of the CloudFormation template.');
});
test('migrate fails when both --from-path and --from-stack are provided', async () => {
const toolkit = defaultToolkitSetup();
await expect(() => toolkit.migrate({
stackName: 'no-source',
fromPath: './here/template.yml',
fromStack: true,
})).rejects.toThrowError('Only one of `--from-path` or `--from-stack` may be provided.');
expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Only one of `--from-path` or `--from-stack` may be provided.');
});
test('migrate fails when --from-path is invalid', async () => {
const toolkit = defaultToolkitSetup();
await expect(() => toolkit.migrate({
stackName: 'bad-local-source',
fromPath: './here/template.yml',
})).rejects.toThrowError('\'./here/template.yml\' is not a valid path.');
expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-local-source`: \'./here/template.yml\' is not a valid path.');
});
test('migrate fails when --from-stack is used and stack does not exist in account', async () => {
const mockSdkProvider = new mock_sdk_1.MockSdkProvider();
mockSdkProvider.stubCloudFormation({
getTemplate(_request) {
throw new Error('Stack does not exist in this environment');
},
});
const mockCloudExecutable = new util_1.MockCloudExecutable({
stacks: [],
});
const cdkToolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable: mockCloudExecutable,
deployments: new deployments_1.Deployments({ sdkProvider: mockSdkProvider }),
sdkProvider: mockSdkProvider,
configuration: mockCloudExecutable.configuration,
});
await expect(() => cdkToolkit.migrate({
stackName: 'bad-cloudformation-source',
fromStack: true,
})).rejects.toThrowError('Stack does not exist in this environment');
expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-cloudformation-source`: Stack does not exist in this environment');
});
test('migrate fails when stack cannot be generated', async () => {
const toolkit = defaultToolkitSetup();
await expect(() => toolkit.migrate({
stackName: 'cannot-generate-template',
fromPath: path.join(__dirname, 'commands', 'test-resources', 'templates', 'sqs-template.json'),
language: 'rust',
})).rejects.toThrowError('stack generation failed due to error \'unreachable\'');
expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `cannot-generate-template`: stack generation failed due to error \'unreachable\'');
});
cliTest('migrate succeeds for valid template from local path when no lanugage is provided', async (workDir) => {
const toolkit = defaultToolkitSetup();
await toolkit.migrate({
stackName: 'SQSTypeScript',
fromPath: sqsTemplatePath,
outputPath: workDir,
});
// Packages created for typescript
expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'package.json'))).toBeTruthy();
expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'bin', 'sqs_type_script.ts'))).toBeTruthy();
expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'lib', 'sqs_type_script-stack.ts'))).toBeTruthy();
});
cliTest('migrate succeeds for valid template from local path when lanugage is provided', async (workDir) => {
const toolkit = defaultToolkitSetup();
await toolkit.migrate({
stackName: 'S3Python',
fromPath: s3TemplatePath,
outputPath: workDir,
language: 'python',
});
// Packages created for typescript
expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'requirements.txt'))).toBeTruthy();
expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'app.py'))).toBeTruthy();
expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 's3_python', 's3_python_stack.py'))).toBeTruthy();
});
cliTest('migrate call is idempotent', async (workDir) => {
const toolkit = defaultToolkitSetup();
await toolkit.migrate({
stackName: 'AutoscalingCSharp',
fromPath: autoscalingTemplatePath,
outputPath: workDir,
language: 'csharp',
});
// Packages created for typescript
expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy();
expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy();
expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy();
// One more time
await toolkit.migrate({
stackName: 'AutoscalingCSharp',
fromPath: autoscalingTemplatePath,
outputPath: workDir,
language: 'csharp',
});
// Packages created for typescript
expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy();
expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy();
expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy();
});
});
describe('stack with error and flagged for validation', () => {
beforeEach(() => {
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
MockStack.MOCK_STACK_A,
MockStack.MOCK_STACK_B,
],
nestedAssemblies: [{
stacks: [
{ properties: { validateOnSynth: true }, ...MockStack.MOCK_STACK_WITH_ERROR },
],
}],
});
});
test('causes synth to fail if autoValidate=true', async () => {
const toolkit = defaultToolkitSetup();
const autoValidate = true;
await expect(toolkit.synth([], false, true, autoValidate)).rejects.toBeDefined();
});
test('causes synth to succeed if autoValidate=false', async () => {
const toolkit = defaultToolkitSetup();
const autoValidate = false;
await toolkit.synth([], false, true, autoValidate);
expect(mockData.mock.calls.length).toEqual(0);
});
});
test('stack has error and was explicitly selected', async () => {
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
MockStack.MOCK_STACK_A,
MockStack.MOCK_STACK_B,
],
nestedAssemblies: [{
stacks: [
{ properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR },
],
}],
});
const toolkit = defaultToolkitSetup();
await expect(toolkit.synth(['Test-Stack-A/witherrors'], false, true)).rejects.toBeDefined();
});
test('stack has error, is not flagged for validation and was not explicitly selected', async () => {
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
MockStack.MOCK_STACK_A,
MockStack.MOCK_STACK_B,
],
nestedAssemblies: [{
stacks: [
{ properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR },
],
}],
});
const toolkit = defaultToolkitSetup();
await toolkit.synth([], false, true);
});
test('stack has dependency and was explicitly selected', async () => {
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
MockStack.MOCK_STACK_C,
MockStack.MOCK_STACK_D,
],
});
const toolkit = defaultToolkitSetup();
await toolkit.synth([MockStack.MOCK_STACK_D.stackName], true, false);
expect(mockData.mock.calls.length).toEqual(1);
expect(mockData.mock.calls[0][0]).toBeDefined();
});
});
class MockStack {
}
MockStack.MOCK_STACK_A = {
stackName: 'Test-Stack-A',
template: { Resources: { TemplateName: 'Test-Stack-A' } },
env: 'aws://123456789012/bermuda-triangle-1',
metadata: {
'/Test-Stack-A': [
{
type: cxschema.ArtifactMetadataEntryType.STACK_TAGS,
data: [
{ key: 'Foo', value: 'Bar' },
],
},
],
},
displayName: 'Test-Stack-A-Display-Name',
};
MockStack.MOCK_STACK_B = {
stackName: 'Test-Stack-B',
template: { Resources: { TemplateName: 'Test-Stack-B' } },
env: 'aws://123456789012/bermuda-triangle-1',
metadata: {
'/Test-Stack-B': [
{
type: cxschema.ArtifactMetadataEntryType.STACK_TAGS,
data: [
{ key: 'Baz', value: 'Zinga!' },
],
},
],
},
};
MockStack.MOCK_STACK_C = {
stackName: 'Test-Stack-C',
template: { Resources: { TemplateName: 'Test-Stack-C' } },
env: 'aws://123456789012/bermuda-triangle-1',
metadata: {
'/Test-Stack-C': [
{
type: cxschema.ArtifactMetadataEntryType.STACK_TAGS,
data: [
{ key: 'Baz', value: 'Zinga!' },
],
},
],
},
displayName: 'Test-Stack-A/Test-Stack-C',
};
MockStack.MOCK_STACK_D = {
stackName: 'Test-Stack-D',
template: { Resources: { TemplateName: 'Test-Stack-D' } },
env: 'aws://123456789012/bermuda-triangle-1',
metadata: {
'/Test-Stack-D': [
{
type: cxschema.ArtifactMetadataEntryType.STACK_TAGS,
data: [
{ key: 'Baz', value: 'Zinga!' },
],
},
],
},
depends: [MockStack.MOCK_STACK_C.stackName],
};
MockStack.MOCK_STACK_WITH_ERROR = {
stackName: 'witherrors',
env: 'aws://123456789012/bermuda-triangle-1',
template: { resource: 'errorresource' },
metadata: {
'/resource': [
{
type: cxschema.ArtifactMetadataEntryType.ERROR,
data: 'this is an error',
},
],
},
displayName: 'Test-Stack-A/witherrors',
};
MockStack.MOCK_STACK_WITH_ASSET = {
stackName: 'Test-Stack-Asset',
template: { Resources: { TemplateName: 'Test-Stack-Asset' } },
env: 'aws://123456789012/bermuda-triangle-1',
assetManifest: {
version: cloud_assembly_schema_1.Manifest.version(),
files: {
xyz: {
source: {
path: path.resolve(__dirname, '..', 'LICENSE'),
},
destinations: {},
},
},
},
};
class FakeCloudFormation extends deployments_1.Deployments {
constructor(expectedTags = {}, expectedNotificationArns) {
super({ sdkProvider: new mock_sdk_1.MockSdkProvider() });
this.expectedTags = {};
for (const [stackName, tags] of Object.entries(expectedTags)) {
this.expectedTags[stackName] =
Object.entries(tags).map(([Key, Value]) => ({ Key, Value }))
.sort((l, r) => l.Key.localeCompare(r.Key));
}
if (expectedNotificationArns) {
this.expectedNotificationArns = expectedNotificationArns;
}
}
deployStack(options) {
expect([
MockStack.MOCK_STACK_A.stackName,
MockStack.MOCK_STACK_B.stackName,
MockStack.MOCK_STACK_C.stackName,
MockStack.MOCK_STACK_WITH_ASSET.stackName,
]).toContain(options.stack.stackName);
if (this.expectedTags[options.stack.stackName]) {
expect(options.tags).toEqual(this.expectedTags[options.stack.stackName]);
}
expect(options.notificationArns).toEqual(this.expectedNotificationArns);
return Promise.resolve({
stackArn: `arn:aws:cloudformation:::stack/${options.stack.stackName}/MockedOut`,
noOp: false,
outputs: { StackName: options.stack.stackName },
stackArtifact: options.stack,
});
}
destroyStack(options) {
expect(options.stack).toBeDefined();
return Promise.resolve();
}
readCurrentTemplate(stack) {
switch (stack.stackName) {
case MockStack.MOCK_STACK_A.stackName:
return Promise.resolve({});
case MockStack.MOCK_STACK_B.stackName:
return Promise.resolve({});
case MockStack.MOCK_STACK_C.stackName:
return Promise.resolve({});
case MockStack.MOCK_STACK_WITH_ASSET.stackName:
return Promise.resolve({});
default:
return Promise.reject(`Not an expected mock stack: ${stack.stackName}`);
}
}
}
function cliTest(name, handler) {
test(name, () => withTempDir(handler));
}
async function withTempDir(cb) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'aws-cdk-test'));
try {
await cb(tmpDir);
}
finally {
await fs.remove(tmpDir);
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2RrLXRvb2xraXQudGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNkay10b29sa2l0LnRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSw0REFBNEQ7QUFDNUQsTUFBTSxxQkFBcUIsR0FBRyxJQUFJLENBQUMsRUFBRSxFQUFFLENBQUM7QUFDeEMsTUFBTSxtQkFBbUIsR0FBRztJQUMxQixFQUFFLEVBQUUscUJBQXFCO0NBQzFCLENBQUM7QUFDRixNQUFNLHFCQUFxQixHQUFHO0lBQzVCLElBQUksYUFBYTtRQUNmLE1BQU0sQ0FBQyxxQkFBcUIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLHNCQUFzQixDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQzFFLGdHQUFnRztRQUNoRyxpRUFBaUU7UUFDakUsaUVBQWlFO1FBQ2pFLE1BQU0sU0FBUyxHQUFHLHFCQUFxQixDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDdEQsMkRBQTJEO1FBQzNELHlCQUF5QjtRQUN6QixNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ25DLHNDQUFzQztRQUN0QyxPQUFPLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN0QixDQUFDO0lBRUQsSUFBSSxpQkFBaUI7UUFDbkIsTUFBTSxDQUFDLHFCQUFxQixDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUMsc0JBQXNCLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDMUUsTUFBTSxVQUFVLEdBQUcscUJBQXFCLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUN2RCwrREFBK0Q7UUFDL0QseUJBQXlCO1FBQ3pCLE1BQU0sQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ3hDLHNDQUFzQztRQUN0QyxPQUFPLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN2QixDQUFDO0NBQ0YsQ0FBQztBQUVGLE1BQU0saUJBQWlCLEdBQUcsSUFBSSxDQUFDLEVBQUUsRUFBRSxDQUFDO0FBQ3BDLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUM7SUFDM0IsS0FBSyxFQUFFLGlCQUFpQjtDQUN6QixDQUFDLENBQUMsQ0FBQztBQUNKLE1BQU0saUJBQWlCLEdBQUc7SUFDeEIsSUFBSSxXQUFXO1FBQ2IsTUFBTSxDQUFDLGlCQUFpQixDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQ3BELGlFQUFpRTtRQUNqRSxPQUFPLGlCQUFpQixDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDNUMsQ0FBQztJQUVELElBQUksV0FBVztRQUNiLE1BQU0sQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNwRCwrRUFBK0U7UUFDL0UsTUFBTSxpQkFBaUIsR0FBRyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQzdELE9BQU8saUJBQWlCLENBQUMsT0FBTyxDQUFDO0lBQ25DLENBQUM