UNPKG

aws-cdk

Version:

CDK Toolkit, the command line tool for CDK apps

1,081 lines 157 kB
"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