UNPKG

aws-cdk

Version:

CDK Toolkit, the command line tool for CDK apps

1,078 lines 131 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const client_cloudformation_1 = require("@aws-sdk/client-cloudformation"); const deployments_1 = require("../../../lib/api/deployments"); const deploy_stack_1 = require("../../../lib/api/deployments/deploy-stack"); const hotswap_deployments_1 = require("../../../lib/api/deployments/hotswap-deployments"); const environment_resources_1 = require("../../../lib/api/environment-resources"); const common_1 = require("../../../lib/api/hotswap/common"); const cli_io_host_1 = require("../../../lib/toolkit/cli-io-host"); const util_1 = require("../../util"); const mock_sdk_1 = require("../../util/mock-sdk"); jest.mock('../../../lib/api/deployments/hotswap-deployments'); jest.mock('../../../lib/api/deployments/checks', () => ({ determineAllowCrossAccountAssetPublishing: jest.fn().mockResolvedValue(true), })); const FAKE_STACK = (0, util_1.testStack)({ stackName: 'withouterrors', }); const FAKE_STACK_WITH_PARAMETERS = (0, util_1.testStack)({ stackName: 'withparameters', template: { Parameters: { HasValue: { Type: 'String' }, HasDefault: { Type: 'String', Default: 'TheDefault' }, OtherParameter: { Type: 'String' }, }, }, }); const FAKE_STACK_TERMINATION_PROTECTION = (0, util_1.testStack)({ stackName: 'termination-protection', template: util_1.DEFAULT_FAKE_TEMPLATE, terminationProtection: true, }); const baseResponse = { StackName: 'mock-stack-name', StackId: 'mock-stack-id', CreationTime: new Date(), StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE, EnableTerminationProtection: false, }; let sdk; let sdkProvider; beforeEach(() => { sdkProvider = new mock_sdk_1.MockSdkProvider(); sdk = new mock_sdk_1.MockSdk(); sdk.getUrlSuffix = () => Promise.resolve('amazonaws.com'); jest.resetAllMocks(); (0, mock_sdk_1.restoreSdkMocksToDefault)(); mock_sdk_1.mockCloudFormationClient .on(client_cloudformation_1.DescribeStacksCommand) // First call, no stacks exis .resolvesOnce({ Stacks: [], }) // Second call, stack has been created .resolves({ Stacks: [ { StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE, StackStatusReason: 'It is magic', EnableTerminationProtection: false, StackName: 'MagicalStack', CreationTime: new Date(), }, ], }); mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeChangeSetCommand).resolves({ Status: client_cloudformation_1.StackStatus.CREATE_COMPLETE, Changes: [], }); mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.GetTemplateCommand).resolves({ TemplateBody: JSON.stringify(util_1.DEFAULT_FAKE_TEMPLATE), }); mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.UpdateTerminationProtectionCommand).resolves({ StackId: 'stack-id', }); }); function standardDeployStackArguments() { const resolvedEnvironment = (0, mock_sdk_1.mockResolvedEnvironment)(); return { stack: FAKE_STACK, sdk, sdkProvider, resolvedEnvironment, envResources: new environment_resources_1.NoBootstrapStackEnvironmentResources(resolvedEnvironment, sdk), }; } test("calls tryHotswapDeployment() if 'hotswap' is `HotswapMode.CLASSIC`", async () => { // WHEN const spyOnSdk = jest.spyOn(sdk, 'appendCustomUserAgent'); await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), hotswap: common_1.HotswapMode.FALL_BACK, extraUserAgent: 'extra-user-agent', }); // THEN expect(hotswap_deployments_1.tryHotswapDeployment).toHaveBeenCalled(); // check that the extra User-Agent is honored expect(spyOnSdk).toHaveBeenCalledWith('extra-user-agent'); // check that the fallback has been called if hotswapping failed expect(spyOnSdk).toHaveBeenCalledWith('cdk-hotswap/fallback'); }); test("calls tryHotswapDeployment() if 'hotswap' is `HotswapMode.HOTSWAP_ONLY`", async () => { // we need the first call to return something in the Stacks prop, // otherwise the access to `stackId` will fail mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [{ ...baseResponse }], }); const spyOnSdk = jest.spyOn(sdk, 'appendCustomUserAgent'); // WHEN const deployStackResult = await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), hotswap: common_1.HotswapMode.HOTSWAP_ONLY, extraUserAgent: 'extra-user-agent', force: true, // otherwise, deployment would be skipped }); // THEN expect(deployStackResult.type === 'did-deploy-stack' && deployStackResult.noOp).toEqual(true); expect(hotswap_deployments_1.tryHotswapDeployment).toHaveBeenCalled(); // check that the extra User-Agent is honored expect(spyOnSdk).toHaveBeenCalledWith('extra-user-agent'); // check that the fallback has not been called if hotswapping failed expect(spyOnSdk).not.toHaveBeenCalledWith('cdk-hotswap/fallback'); }); test('correctly passes CFN parameters when hotswapping', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), hotswap: common_1.HotswapMode.FALL_BACK, parameters: { A: 'A-value', B: 'B=value', C: undefined, D: '', }, }); // THEN expect(hotswap_deployments_1.tryHotswapDeployment).toHaveBeenCalledWith(expect.anything(), { A: 'A-value', B: 'B=value' }, expect.anything(), expect.anything(), common_1.HotswapMode.FALL_BACK, expect.anything()); }); test('correctly passes SSM parameters when hotswapping', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Parameters: [{ ParameterKey: 'SomeParameter', ParameterValue: 'ParameterName', ResolvedValue: 'SomeValue' }], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: (0, util_1.testStack)({ stackName: 'stack', template: { Parameters: { SomeParameter: { Type: 'AWS::SSM::Parameter::Value<String>', Default: 'ParameterName', }, }, }, }), hotswap: common_1.HotswapMode.FALL_BACK, usePreviousParameters: true, }); // THEN expect(hotswap_deployments_1.tryHotswapDeployment).toHaveBeenCalledWith(expect.anything(), { SomeParameter: 'SomeValue' }, expect.anything(), expect.anything(), common_1.HotswapMode.FALL_BACK, expect.anything()); }); test('call CreateStack when method=direct and the stack doesnt exist yet', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'direct' }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.CreateStackCommand); }); test('call UpdateStack when method=direct and the stack exists already', async () => { // WHEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [{ ...baseResponse }], }); await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'direct' }, force: true, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.UpdateStackCommand); }); test('method=direct and no updates to be performed', async () => { const error = new Error('No updates are to be performed.'); error.name = 'ValidationError'; mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.UpdateStackCommand).rejectsOnce(error); // WHEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [{ ...baseResponse }], }); const ret = await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'direct' }, force: true, }); // THEN expect(ret).toEqual(expect.objectContaining({ noOp: true })); }); test("does not call tryHotswapDeployment() if 'hotswap' is false", async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), hotswap: undefined, }); // THEN expect(hotswap_deployments_1.tryHotswapDeployment).not.toHaveBeenCalled(); }); test("rollback still defaults to enabled even if 'hotswap' is enabled", async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), hotswap: common_1.HotswapMode.FALL_BACK, rollback: undefined, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommandWith(client_cloudformation_1.ExecuteChangeSetCommand, expect.objectContaining({ DisableRollback: true, })); }); test("rollback defaults to enabled if 'hotswap' is undefined", async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), hotswap: undefined, rollback: undefined, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandTimes(client_cloudformation_1.ExecuteChangeSetCommand, 1); expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommandWith(client_cloudformation_1.ExecuteChangeSetCommand, expect.objectContaining({ DisableRollback: true, })); }); test('do deploy executable change set with 0 changes', async () => { // WHEN const ret = await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(ret.type === 'did-deploy-stack' && ret.noOp).toBeFalsy(); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('correctly passes CFN parameters, ignoring ones with empty values', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), parameters: { A: 'A-value', B: 'B=value', C: undefined, D: '', }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, Parameters: [ { ParameterKey: 'A', ParameterValue: 'A-value' }, { ParameterKey: 'B', ParameterValue: 'B=value' }, ], TemplateBody: expect.any(String), }); }); test('reuse previous parameters if requested', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Parameters: [ { ParameterKey: 'HasValue', ParameterValue: 'TheValue' }, { ParameterKey: 'HasDefault', ParameterValue: 'TheOldValue' }, ], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK_WITH_PARAMETERS, parameters: { OtherParameter: 'SomeValue', }, usePreviousParameters: true, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, Parameters: [ { ParameterKey: 'HasValue', UsePreviousValue: true }, { ParameterKey: 'HasDefault', UsePreviousValue: true }, { ParameterKey: 'OtherParameter', ParameterValue: 'SomeValue' }, ], }); }); describe('ci=true', () => { let stderrMock; let stdoutMock; beforeEach(() => { cli_io_host_1.CliIoHost.instance().isCI = true; jest.resetAllMocks(); stderrMock = jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; }); stdoutMock = jest.spyOn(process.stdout, 'write').mockImplementation(() => { return true; }); }); afterEach(() => { cli_io_host_1.CliIoHost.instance().isCI = false; }); test('output written to stdout', async () => { // GIVEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(stderrMock.mock.calls).toEqual([]); expect(stdoutMock.mock.calls).not.toEqual([]); }); }); test('do not reuse previous parameters if not requested', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Parameters: [ { ParameterKey: 'HasValue', ParameterValue: 'TheValue' }, { ParameterKey: 'HasDefault', ParameterValue: 'TheOldValue' }, ], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK_WITH_PARAMETERS, parameters: { HasValue: 'SomeValue', OtherParameter: 'SomeValue', }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ChangeSetType: client_cloudformation_1.ChangeSetType.UPDATE, Parameters: [ { ParameterKey: 'HasValue', ParameterValue: 'SomeValue' }, { ParameterKey: 'OtherParameter', ParameterValue: 'SomeValue' }, ], }); }); test('throw exception if not enough parameters supplied', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Parameters: [ { ParameterKey: 'HasValue', ParameterValue: 'TheValue' }, { ParameterKey: 'HasDefault', ParameterValue: 'TheOldValue' }, ], }, ], }); // WHEN await expect((0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK_WITH_PARAMETERS, parameters: { OtherParameter: 'SomeValue', }, })).rejects.toThrow(/CloudFormation Parameters are missing a value/); }); test('deploy is skipped if template did not change', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('deploy is skipped if parameters are the same', async () => { // GIVEN givenTemplateIs(FAKE_STACK_WITH_PARAMETERS.template); mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Parameters: [ { ParameterKey: 'HasValue', ParameterValue: 'TheValue' }, { ParameterKey: 'HasDefault', ParameterValue: 'TheOldValue' }, { ParameterKey: 'OtherParameter', ParameterValue: 'OtherParameter' }, ], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK_WITH_PARAMETERS, parameters: {}, usePreviousParameters: true, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); }); test('deploy is not skipped if parameters are different', async () => { // GIVEN givenTemplateIs(FAKE_STACK_WITH_PARAMETERS.template); mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Parameters: [ { ParameterKey: 'HasValue', ParameterValue: 'TheValue' }, { ParameterKey: 'HasDefault', ParameterValue: 'TheOldValue' }, { ParameterKey: 'OtherParameter', ParameterValue: 'OtherParameter' }, ], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK_WITH_PARAMETERS, parameters: { HasValue: 'NewValue', }, usePreviousParameters: true, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ChangeSetType: client_cloudformation_1.ChangeSetType.UPDATE, Parameters: [ { ParameterKey: 'HasValue', ParameterValue: 'NewValue' }, { ParameterKey: 'HasDefault', UsePreviousValue: true }, { ParameterKey: 'OtherParameter', UsePreviousValue: true }, ], }); }); test('deploy is skipped if notificationArns are the same', async () => { // GIVEN givenTemplateIs(FAKE_STACK.template); givenStackExists({ NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK, notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], }); // THEN expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); }); test('deploy is not skipped if notificationArns are different', async () => { // GIVEN givenTemplateIs(FAKE_STACK.template); givenStackExists({ NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK, notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:MagicTopic'], }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); }); test('if existing stack failed to create, it is deleted and recreated', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient .on(client_cloudformation_1.DescribeStacksCommand) .resolvesOnce({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.ROLLBACK_COMPLETE, }, ], }) .resolvesOnce({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.DELETE_COMPLETE, }, ], }) .resolves({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE, }, ], }); givenTemplateIs({ DifferentThan: 'TheDefault', }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.DeleteStackCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ChangeSetType: client_cloudformation_1.ChangeSetType.CREATE, }); }); test('if existing stack failed to create, it is deleted and recreated even if the template did not change', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient .on(client_cloudformation_1.DescribeStacksCommand) .resolvesOnce({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.ROLLBACK_COMPLETE, }, ], }) .resolvesOnce({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.DELETE_COMPLETE, }, ], }) .resolves({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE, }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.DeleteStackCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ChangeSetType: client_cloudformation_1.ChangeSetType.CREATE, }); }); test('deploy not skipped if template did not change and --force is applied', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [{ ...baseResponse }], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), force: true, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandTimes(client_cloudformation_1.ExecuteChangeSetCommand, 1); }); test('deploy is skipped if template and tags did not change', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Tags: [ { Key: 'Key1', Value: 'Value1' }, { Key: 'Key2', Value: 'Value2' }, ], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), tags: [ { Key: 'Key1', Value: 'Value1' }, { Key: 'Key2', Value: 'Value2' }, ], }); // THEN expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.DescribeStacksCommand, { StackName: 'withouterrors', }); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.GetTemplateCommand, { StackName: 'withouterrors', TemplateStage: 'Original', }); }); test('deploy not skipped if template did not change but tags changed', async () => { // GIVEN givenStackExists({ Tags: [{ Key: 'Key', Value: 'Value' }], }); // WHEN const resolvedEnvironment = (0, mock_sdk_1.mockResolvedEnvironment)(); await (0, deploy_stack_1.deployStack)({ stack: FAKE_STACK, sdk, sdkProvider, resolvedEnvironment, tags: [ { Key: 'Key', Value: 'NewValue', }, ], envResources: new environment_resources_1.NoBootstrapStackEnvironmentResources(resolvedEnvironment, sdk), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.DescribeChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.DescribeStacksCommand, { StackName: 'withouterrors', }); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.GetTemplateCommand, { StackName: 'withouterrors', TemplateStage: 'Original', }); }); test('deployStack reports no change if describeChangeSet returns specific error', async () => { mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeChangeSetCommand).resolvesOnce({ Status: client_cloudformation_1.ChangeSetStatus.FAILED, StatusReason: 'No updates are to be performed.', }); // WHEN const deployResult = await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(deployResult.type === 'did-deploy-stack' && deployResult.noOp).toEqual(true); }); test('deploy not skipped if template did not change but one tag removed', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Tags: [ { Key: 'Key1', Value: 'Value1' }, { Key: 'Key2', Value: 'Value2' }, ], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), tags: [{ Key: 'Key1', Value: 'Value1' }], }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.DescribeChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.DescribeStacksCommand, { StackName: 'withouterrors', }); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.GetTemplateCommand, { StackName: 'withouterrors', TemplateStage: 'Original', }); }); test('deploy is not skipped if stack is in a _FAILED state', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.DELETE_FAILED, }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), usePreviousParameters: true, }).catch(() => { }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('existing stack in UPDATE_ROLLBACK_COMPLETE state can be updated', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient .on(client_cloudformation_1.DescribeStacksCommand) .resolvesOnce({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.UPDATE_ROLLBACK_COMPLETE, }, ], }) .resolves({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.UPDATE_COMPLETE, }, ], }); givenTemplateIs({ changed: 123 }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.DeleteStackCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ChangeSetType: client_cloudformation_1.ChangeSetType.UPDATE, }); }); test('deploy not skipped if template changed', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [{ ...baseResponse }], }); givenTemplateIs({ changed: 123 }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('not executed and no error if --no-execute is given', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'change-set', execute: false }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('empty change set is deleted if --execute is given', async () => { mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeChangeSetCommand).resolvesOnce({ Status: client_cloudformation_1.ChangeSetStatus.FAILED, StatusReason: 'No updates are to be performed.', }); // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [{ ...baseResponse }], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'change-set', execute: true }, force: true, // Necessary to bypass "skip deploy" }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); //the first deletion is for any existing cdk change sets, the second is for the deleting the new empty change set expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandTimes(client_cloudformation_1.DeleteChangeSetCommand, 2); }); test('empty change set is not deleted if --no-execute is given', async () => { mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeChangeSetCommand).resolvesOnce({ Status: client_cloudformation_1.ChangeSetStatus.FAILED, StatusReason: 'No updates are to be performed.', }); // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [{ ...baseResponse }], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'change-set', execute: false }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.CreateChangeSetCommand); expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); //the first deletion is for any existing cdk change sets expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandTimes(client_cloudformation_1.DeleteChangeSetCommand, 1); }); test('use S3 url for stack deployment if present in Stack Artifact', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: (0, util_1.testStack)({ stackName: 'withouterrors', properties: { stackTemplateAssetObjectUrl: 'https://use-me-use-me/', }, }), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, TemplateURL: 'https://use-me-use-me/', }); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('use REST API S3 url with substituted placeholders if manifest url starts with s3://', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: (0, util_1.testStack)({ stackName: 'withouterrors', properties: { stackTemplateAssetObjectUrl: 's3://use-me-use-me-${AWS::AccountId}/object', }, }), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, TemplateURL: 'https://s3.bermuda-triangle-1337.amazonaws.com/use-me-use-me-123456789/object', }); expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('changeset is created when stack exists in REVIEW_IN_PROGRESS status', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, StackStatus: client_cloudformation_1.StackStatus.REVIEW_IN_PROGRESS, Tags: [ { Key: 'Key1', Value: 'Value1' }, { Key: 'Key2', Value: 'Value2' }, ], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'change-set', execute: false }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ChangeSetType: client_cloudformation_1.ChangeSetType.CREATE, StackName: 'withouterrors', }); expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('changeset is updated when stack exists in CREATE_COMPLETE status', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, Tags: [ { Key: 'Key1', Value: 'Value1' }, { Key: 'Key2', Value: 'Value2' }, ], }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'change-set', execute: false }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ChangeSetType: client_cloudformation_1.ChangeSetType.UPDATE, StackName: 'withouterrors', }); expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.ExecuteChangeSetCommand); }); test('deploy with termination protection enabled', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK_TERMINATION_PROTECTION, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.UpdateTerminationProtectionCommand, { StackName: 'termination-protection', EnableTerminationProtection: true, }); }); test('updateTerminationProtection not called when termination protection is undefined', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommand(client_cloudformation_1.UpdateTerminationProtectionCommand); }); test('updateTerminationProtection called when termination protection is undefined and stack has termination protection', async () => { // GIVEN mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolves({ Stacks: [ { ...baseResponse, EnableTerminationProtection: true, }, ], }); // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.UpdateTerminationProtectionCommand, { StackName: 'withouterrors', EnableTerminationProtection: false, }); }); describe('disable rollback', () => { test('by default, we do not disable rollback (and also do not pass the flag)', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandTimes(client_cloudformation_1.ExecuteChangeSetCommand, 1); expect(mock_sdk_1.mockCloudFormationClient).not.toHaveReceivedCommandWith(client_cloudformation_1.ExecuteChangeSetCommand, { DisableRollback: expect.anything, ChangeSetName: expect.any(String), }); }); test('rollback can be disabled by setting rollback: false', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), rollback: false, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.ExecuteChangeSetCommand, { ...expect.anything, DisableRollback: true, }); }); }); describe('import-existing-resources', () => { test('is disabled by default', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'change-set', }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ImportExistingResources: false, }); }); test('is added to the CreateChangeSetCommandInput', async () => { // WHEN await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), deploymentMethod: { method: 'change-set', importExistingResources: true, }, }); // THEN expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.CreateChangeSetCommand, { ...expect.anything, ImportExistingResources: true, }); }); }); test.each([ // From a failed state, a --no-rollback is possible as long as there is not a replacement [client_cloudformation_1.StackStatus.UPDATE_FAILED, 'no-rollback', 'no-replacement', 'did-deploy-stack'], [client_cloudformation_1.StackStatus.UPDATE_FAILED, 'no-rollback', 'replacement', 'failpaused-need-rollback-first'], // Any combination of UPDATE_FAILED & rollback always requires a rollback first [client_cloudformation_1.StackStatus.UPDATE_FAILED, 'rollback', 'replacement', 'failpaused-need-rollback-first'], [client_cloudformation_1.StackStatus.UPDATE_FAILED, 'rollback', 'no-replacement', 'failpaused-need-rollback-first'], // From a stable state, any deployment containing a replacement requires a regular deployment (--rollback) [client_cloudformation_1.StackStatus.UPDATE_COMPLETE, 'no-rollback', 'replacement', 'replacement-requires-rollback'], ])('no-rollback and replacement is disadvised: %s %s %s -> %s', async (stackStatus, rollback, replacement, expectedType) => { // GIVEN givenTemplateIs(FAKE_STACK.template); givenStackExists({ // First call StackStatus: stackStatus, }, { // Later calls StackStatus: 'UPDATE_COMPLETE', }); givenChangeSetContainsReplacement(replacement === 'replacement'); // WHEN const result = await (0, deploy_stack_1.deployStack)({ ...standardDeployStackArguments(), stack: FAKE_STACK, rollback: rollback === 'rollback', force: true, // Bypass 'canSkipDeploy' }); // THEN expect(result.type).toEqual(expectedType); }); test('assertIsSuccessfulDeployStackResult does what it says', () => { expect(() => (0, deployments_1.assertIsSuccessfulDeployStackResult)({ type: 'replacement-requires-rollback' })).toThrow(); }); /** * Set up the mocks so that it looks like the stack exists to start with * * The last element of this array will be continuously repeated. */ function givenStackExists(...overrides) { if (overrides.length === 0) { overrides = [{}]; } let handler = mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand); for (const override of overrides.slice(0, overrides.length - 1)) { handler = handler.resolvesOnce({ Stacks: [{ ...baseResponse, ...override }], }); } handler.resolves({ Stacks: [{ ...baseResponse, ...overrides[overrides.length - 1] }], }); } function givenTemplateIs(template) { mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.GetTemplateCommand).resolves({ TemplateBody: JSON.stringify(template), }); } function givenChangeSetContainsReplacement(replacement) { mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeChangeSetCommand).resolves({ Status: 'CREATE_COMPLETE', Changes: replacement ? [ { Type: 'Resource', ResourceChange: { PolicyAction: 'ReplaceAndDelete', Action: 'Modify', LogicalResourceId: 'Queue4A7E3555', PhysicalResourceId: 'https://sqs.eu-west-1.amazonaws.com/111111111111/Queue4A7E3555-P9C8nK3uv8v6.fifo', ResourceType: 'AWS::SQS::Queue', Replacement: 'True', Scope: ['Properties'], Details: [ { Target: { Attribute: 'Properties', Name: 'FifoQueue', RequiresRecreation: 'Always', }, Evaluation: 'Static', ChangeSource: 'DirectModification', }, ], }, }, ] : [], }); } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwbG95LXN0YWNrLnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJkZXBsb3ktc3RhY2sudGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLDBFQWlCd0M7QUFDeEMsOERBQW1GO0FBQ25GLDRFQUE0RjtBQUM1RiwwRkFBd0Y7QUFDeEYsa0ZBQThGO0FBQzlGLDREQUE4RDtBQUM5RCxrRUFBNkQ7QUFDN0QscUNBQThEO0FBQzlELGtEQU02QjtBQUU3QixJQUFJLENBQUMsSUFBSSxDQUFDLGtEQUFrRCxDQUFDLENBQUM7QUFDOUQsSUFBSSxDQUFDLElBQUksQ0FBQyxxQ0FBcUMsRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO0lBQ3RELHlDQUF5QyxFQUFFLElBQUksQ0FBQyxFQUFFLEVBQUUsQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUM7Q0FDN0UsQ0FBQyxDQUFDLENBQUM7QUFFSixNQUFNLFVBQVUsR0FBRyxJQUFBLGdCQUFTLEVBQUM7SUFDM0IsU0FBUyxFQUFFLGVBQWU7Q0FDM0IsQ0FBQyxDQUFDO0FBRUgsTUFBTSwwQkFBMEIsR0FBRyxJQUFBLGdCQUFTLEVBQUM7SUFDM0MsU0FBUyxFQUFFLGdCQUFnQjtJQUMzQixRQUFRLEVBQUU7UUFDUixVQUFVLEVBQUU7WUFDVixRQUFRLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFO1lBQzVCLFVBQVUsRUFBRSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsT0FBTyxFQUFFLFlBQVksRUFBRTtZQUNyRCxjQUFjLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFO1NBQ25DO0tBQ0Y7Q0FDRixDQUFDLENBQUM7QUFFSCxNQUFNLGlDQUFpQyxHQUFHLElBQUEsZ0JBQVMsRUFBQztJQUNsRCxTQUFTLEVBQUUsd0JBQXdCO0lBQ25DLFFBQVEsRUFBRSw0QkFBcUI7SUFDL0IscUJBQXFCLEVBQUUsSUFBSTtDQUM1QixDQUFDLENBQUM7QUFFSCxNQUFNLFlBQVksR0FBRztJ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