aws-cdk
Version:
CDK Toolkit, the command line tool for CDK apps
1,078 lines • 131 kB
JavaScript
"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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