aws-cdk
Version:
CDK Toolkit, the command line tool for CDK apps
1,017 lines • 110 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
jest.mock('../../../lib/api/deployments/deploy-stack');
jest.mock('../../../lib/api/deployments/asset-publishing');
const client_cloudformation_1 = require("@aws-sdk/client-cloudformation");
const client_ssm_1 = require("@aws-sdk/client-ssm");
const deployments_1 = require("../../../lib/api/deployments");
const deploy_stack_1 = require("../../../lib/api/deployments/deploy-stack");
const common_1 = require("../../../lib/api/hotswap/common");
const toolkit_info_1 = require("../../../lib/api/toolkit-info");
const util_1 = require("../../util");
const mock_sdk_1 = require("../../util/mock-sdk");
const fake_cloudformation_stack_1 = require("../fake-cloudformation-stack");
let sdkProvider;
let sdk;
let deployments;
let mockToolkitInfoLookup;
let currentCfnStackResources;
beforeEach(() => {
jest.resetAllMocks();
sdkProvider = new mock_sdk_1.MockSdkProvider();
sdk = new mock_sdk_1.MockSdk();
deployments = new deployments_1.Deployments({ sdkProvider });
currentCfnStackResources = {};
(0, mock_sdk_1.restoreSdkMocksToDefault)();
toolkit_info_1.ToolkitInfo.lookup = mockToolkitInfoLookup = jest
.fn()
.mockResolvedValue(toolkit_info_1.ToolkitInfo.bootstrapStackNotFoundInfo('TestBootstrapStack'));
(0, mock_sdk_1.setDefaultSTSMocks)();
});
function mockSuccessfulBootstrapStackLookup(props) {
const outputs = {
BucketName: 'BUCKET_NAME',
BucketDomainName: 'BUCKET_ENDPOINT',
BootstrapVersion: '1',
...props,
};
const fakeStack = (0, mock_sdk_1.mockBootstrapStack)({
Outputs: Object.entries(outputs).map(([k, v]) => ({
OutputKey: k,
OutputValue: `${v}`,
})),
});
mockToolkitInfoLookup.mockResolvedValue(toolkit_info_1.ToolkitInfo.fromStack(fakeStack));
}
test('passes through hotswap=true to deployStack()', async () => {
// WHEN
await deployments.deployStack({
stack: (0, util_1.testStack)({
stackName: 'boop',
}),
hotswap: common_1.HotswapMode.FALL_BACK,
});
// THEN
expect(deploy_stack_1.deployStack).toHaveBeenCalledWith(expect.objectContaining({
hotswap: common_1.HotswapMode.FALL_BACK,
}));
});
test('placeholders are substituted in CloudFormation execution role', async () => {
await deployments.deployStack({
stack: (0, util_1.testStack)({
stackName: 'boop',
properties: {
cloudFormationExecutionRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}',
},
}),
});
expect(deploy_stack_1.deployStack).toHaveBeenCalledWith(expect.objectContaining({
roleArn: 'bloop:here:123456789012',
}));
});
test('role with placeholders is assumed if assumerole is given', async () => {
const mockForEnvironment = jest.fn().mockImplementation(() => {
return { sdk: new mock_sdk_1.MockSdk() };
});
sdkProvider.forEnvironment = mockForEnvironment;
await deployments.deployStack({
stack: (0, util_1.testStack)({
stackName: 'boop',
properties: {
assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}',
},
}),
});
expect(mockForEnvironment).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({
assumeRoleArn: 'bloop:here:123456789012',
}));
});
test('deployment fails if bootstrap stack is missing', async () => {
await expect(deployments.deployStack({
stack: (0, util_1.testStack)({
stackName: 'boop',
properties: {
assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}',
requiresBootstrapStackVersion: 99,
},
}),
})).rejects.toThrow(/requires a bootstrap stack/);
});
test('deployment fails if bootstrap stack is too old', async () => {
mockSuccessfulBootstrapStackLookup({
BootstrapVersion: 5,
});
(0, mock_sdk_1.setDefaultSTSMocks)();
await expect(deployments.deployStack({
stack: (0, util_1.testStack)({
stackName: 'boop',
properties: {
assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}',
requiresBootstrapStackVersion: 99,
},
}),
})).rejects.toThrow(/requires bootstrap stack version '99', found '5'/);
});
test.each([false, true])('if toolkit stack be found: %p but SSM parameter name is present deployment succeeds', async (canLookup) => {
if (canLookup) {
mockSuccessfulBootstrapStackLookup({
BootstrapVersion: 2,
});
}
(0, mock_sdk_1.setDefaultSTSMocks)();
mock_sdk_1.mockSSMClient.on(client_ssm_1.GetParameterCommand).resolves({
Parameter: {
Value: '99',
},
});
await deployments.deployStack({
stack: (0, util_1.testStack)({
stackName: 'boop',
properties: {
assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}',
requiresBootstrapStackVersion: 99,
bootstrapStackVersionSsmParameter: '/some/parameter',
},
}),
});
expect(mock_sdk_1.mockSSMClient).toHaveReceivedCommandWith(client_ssm_1.GetParameterCommand, {
Name: '/some/parameter',
});
});
test('readCurrentTemplateWithNestedStacks() can handle non-Resources in the template', async () => {
const stackSummary = stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd');
pushStackResourceSummaries('ParentOfStackWithOutputAndParameter', stackSummary);
mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.ListStackResourcesCommand).resolvesOnce({
StackResourceSummaries: [stackSummary],
});
mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStacksCommand).resolvesOnce({
Stacks: [
{
StackName: 'NestedStack',
RootId: 'StackId',
CreationTime: new Date(),
StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE,
},
],
});
const cfnStack = new fake_cloudformation_stack_1.FakeCloudformationStack({
stackName: 'ParentOfStackWithOutputAndParameter',
stackId: 'StackId',
});
deployments_1.CloudFormationStack.lookup = async (_, stackName) => {
switch (stackName) {
case 'ParentOfStackWithOutputAndParameter':
cfnStack.template = async () => ({
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-output-one-param-stack.nested.template.json',
},
},
},
});
break;
case 'NestedStack':
cfnStack.template = async () => ({
Resources: {
NestedResource: {
Type: 'AWS::Something',
Properties: {
Property: 'old-value',
},
},
},
Parameters: {
NestedParam: {
Type: 'String',
},
},
Outputs: {
NestedOutput: {
Value: {
Ref: 'NestedResource',
},
},
},
});
break;
default:
throw new Error('unknown stack name ' + stackName + ' found');
}
return cfnStack;
};
const rootStack = (0, util_1.testStack)({
stackName: 'ParentOfStackWithOutputAndParameter',
template: {
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-output-one-param-stack.nested.template.json',
},
},
},
},
});
// WHEN
const rootTemplate = await deployments.readCurrentTemplateWithNestedStacks(rootStack);
const deployedTemplate = rootTemplate.deployedRootTemplate;
const nestedStacks = rootTemplate.nestedStacks;
// THEN
expect(deployedTemplate).toEqual({
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-output-one-param-stack.nested.template.json',
},
},
},
});
expect(rootStack.template).toEqual({
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-output-one-param-stack.nested.template.json',
},
},
},
});
expect(nestedStacks).toEqual({
NestedStack: {
deployedTemplate: {
Outputs: {
NestedOutput: {
Value: {
Ref: 'NestedResource',
},
},
},
Parameters: {
NestedParam: {
Type: 'String',
},
},
Resources: {
NestedResource: {
Properties: {
Property: 'old-value',
},
Type: 'AWS::Something',
},
},
},
generatedTemplate: {
Outputs: {
NestedOutput: {
Value: {
Ref: 'NestedResource',
},
},
},
Parameters: {
NestedParam: {
Type: 'Number',
},
},
Resources: {
NestedResource: {
Properties: {
Property: 'new-value',
},
Type: 'AWS::Something',
},
},
},
nestedStackTemplates: {},
physicalName: 'NestedStack',
},
});
});
test('readCurrentTemplateWithNestedStacks() with a 3-level nested + sibling structure works', async () => {
const rootSummary = stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd');
const nestedStackSummary = [
stackSummaryOf('GrandChildStackA', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStackA/abcd'),
stackSummaryOf('GrandChildStackB', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStackB/abcd'),
];
const grandChildAStackSummary = stackSummaryOf('GrandChildA', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildA/abcd');
const grandchildBStackSummary = stackSummaryOf('GrandChildB', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildB/abcd');
pushStackResourceSummaries('MultiLevelRoot', rootSummary);
pushStackResourceSummaries('NestedStack', ...nestedStackSummary);
pushStackResourceSummaries('GrandChildStackA', grandChildAStackSummary);
pushStackResourceSummaries('GrandChildStackB', grandchildBStackSummary);
mock_sdk_1.mockCloudFormationClient
.on(client_cloudformation_1.ListStackResourcesCommand)
.resolvesOnce({
StackResourceSummaries: [rootSummary],
})
.resolvesOnce({
StackResourceSummaries: nestedStackSummary,
})
.resolvesOnce({
StackResourceSummaries: [grandChildAStackSummary],
})
.resolvesOnce({
StackResourceSummaries: [grandchildBStackSummary],
});
mock_sdk_1.mockCloudFormationClient
.on(client_cloudformation_1.DescribeStacksCommand)
.resolvesOnce({
Stacks: [
{
StackName: 'NestedStack',
RootId: 'StackId',
CreationTime: new Date(),
StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE,
},
],
})
.resolvesOnce({
Stacks: [
{
StackName: 'GrandChildStackA',
RootId: 'StackId',
ParentId: 'NestedStack',
CreationTime: new Date(),
StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE,
},
],
})
.resolvesOnce({
Stacks: [
{
StackName: 'GrandChildStackB',
RootId: 'StackId',
ParentId: 'NestedStack',
CreationTime: new Date(),
StackStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE,
},
],
});
givenStacks({
MultiLevelRoot: {
template: {
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-two-stacks-stack.nested.template.json',
},
},
},
},
},
NestedStack: {
template: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Property: 'old-value',
},
},
GrandChildStackA: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
GrandChildStackB: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
},
},
},
GrandChildStackA: {
template: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Property: 'old-value',
},
},
},
},
},
GrandChildStackB: {
template: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Property: 'old-value',
},
},
},
},
},
});
const rootStack = (0, util_1.testStack)({
stackName: 'MultiLevelRoot',
template: {
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-two-stacks-stack.nested.template.json',
},
},
},
},
});
// WHEN
const rootTemplate = await deployments.readCurrentTemplateWithNestedStacks(rootStack);
const deployedTemplate = rootTemplate.deployedRootTemplate;
const nestedStacks = rootTemplate.nestedStacks;
// THEN
expect(deployedTemplate).toEqual({
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-two-stacks-stack.nested.template.json',
},
},
},
});
expect(rootStack.template).toEqual({
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-two-stacks-stack.nested.template.json',
},
},
},
});
expect(nestedStacks).toEqual({
NestedStack: {
deployedTemplate: {
Resources: {
GrandChildStackA: {
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Type: 'AWS::CloudFormation::Stack',
},
GrandChildStackB: {
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Type: 'AWS::CloudFormation::Stack',
},
SomeResource: {
Properties: {
Property: 'old-value',
},
Type: 'AWS::Something',
},
},
},
generatedTemplate: {
Resources: {
GrandChildStackA: {
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Type: 'AWS::CloudFormation::Stack',
},
GrandChildStackB: {
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Type: 'AWS::CloudFormation::Stack',
},
SomeResource: {
Properties: {
Property: 'new-value',
},
Type: 'AWS::Something',
},
},
},
nestedStackTemplates: {
GrandChildStackA: {
deployedTemplate: {
Resources: {
SomeResource: {
Properties: {
Property: 'old-value',
},
Type: 'AWS::Something',
},
},
},
generatedTemplate: {
Resources: {
SomeResource: {
Properties: {
Property: 'new-value',
},
Type: 'AWS::Something',
},
},
},
nestedStackTemplates: {},
physicalName: 'GrandChildStackA',
},
GrandChildStackB: {
deployedTemplate: {
Resources: {
SomeResource: {
Properties: {
Property: 'old-value',
},
Type: 'AWS::Something',
},
},
},
generatedTemplate: {
Resources: {
SomeResource: {
Properties: {
Property: 'new-value',
},
Type: 'AWS::Something',
},
},
},
nestedStackTemplates: {},
physicalName: 'GrandChildStackB',
},
},
physicalName: 'NestedStack',
},
});
});
test('readCurrentTemplateWithNestedStacks() on an undeployed parent stack with an (also undeployed) nested stack works', async () => {
// GIVEN
const cfnStack = new fake_cloudformation_stack_1.FakeCloudformationStack({
stackName: 'UndeployedParent',
stackId: 'StackId',
});
deployments_1.CloudFormationStack.lookup = async (_cfn, _stackName) => {
cfnStack.template = async () => ({});
return cfnStack;
};
const rootStack = (0, util_1.testStack)({
stackName: 'UndeployedParent',
template: {
Resources: {
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-one-stack-stack.nested.template.json',
},
},
},
},
});
// WHEN
const deployedTemplate = (await deployments.readCurrentTemplateWithNestedStacks(rootStack)).deployedRootTemplate;
const nestedStacks = (await deployments.readCurrentTemplateWithNestedStacks(rootStack)).nestedStacks;
// THEN
expect(deployedTemplate).toEqual({});
expect(nestedStacks).toEqual({
NestedStack: {
deployedTemplate: {},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Property: 'new-value',
},
},
NestedStack: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
},
},
nestedStackTemplates: {
NestedStack: {
deployedTemplate: {},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Property: 'new-value',
},
},
},
},
nestedStackTemplates: {},
},
},
},
});
});
test('readCurrentTemplateWithNestedStacks() caches calls to listStackResources()', async () => {
// GIVEN
givenStacks({
'*': {
template: {
Resources: {
NestedStackA: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
NestedStackB: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
},
},
},
});
const rootStack = (0, util_1.testStack)({
stackName: 'CachingRoot',
template: {
Resources: {
NestedStackA: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
NestedStackB: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
},
},
});
pushStackResourceSummaries('CachingRoot', stackSummaryOf('NestedStackA', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/one-resource-stack/abcd'), stackSummaryOf('NestedStackB', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/one-resource-stack/abcd'));
// WHEN
await deployments.readCurrentTemplateWithNestedStacks(rootStack);
// THEN
expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandTimes(client_cloudformation_1.ListStackResourcesCommand, 1);
});
test('rollback stack assumes role if necessary', async () => {
const mockForEnvironment = jest.fn().mockImplementation(() => {
return { sdk };
});
sdkProvider.forEnvironment = mockForEnvironment;
givenStacks({
'*': { template: {} },
});
await deployments.rollbackStack({
stack: (0, util_1.testStack)({
stackName: 'boop',
properties: {
assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}',
},
}),
validateBootstrapStackVersion: false,
});
expect(mockForEnvironment).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({
assumeRoleArn: 'bloop:here:123456789012',
}));
});
test('rollback stack allows rolling back from UPDATE_FAILED', async () => {
// GIVEN
givenStacks({
'*': { template: {}, stackStatus: 'UPDATE_FAILED' },
});
// WHEN
await deployments.rollbackStack({
stack: (0, util_1.testStack)({ stackName: 'boop' }),
validateBootstrapStackVersion: false,
});
// THEN
expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.RollbackStackCommand);
});
test('rollback stack allows continue rollback from UPDATE_ROLLBACK_FAILED', async () => {
// GIVEN
givenStacks({
'*': { template: {}, stackStatus: 'UPDATE_ROLLBACK_FAILED' },
});
// WHEN
await deployments.rollbackStack({
stack: (0, util_1.testStack)({ stackName: 'boop' }),
validateBootstrapStackVersion: false,
});
// THEN
expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommand(client_cloudformation_1.ContinueUpdateRollbackCommand);
});
test('rollback stack fails in UPDATE_COMPLETE state', async () => {
// GIVEN
givenStacks({
'*': { template: {}, stackStatus: 'UPDATE_COMPLETE' },
});
// WHEN
const response = await deployments.rollbackStack({
stack: (0, util_1.testStack)({ stackName: 'boop' }),
validateBootstrapStackVersion: false,
});
// THEN
expect(response.notInRollbackableState).toBe(true);
});
test('continue rollback stack with force ignores any failed resources', async () => {
// GIVEN
givenStacks({
'*': { template: {}, stackStatus: 'UPDATE_ROLLBACK_FAILED' },
});
mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.DescribeStackEventsCommand).resolves({
StackEvents: [
{
EventId: 'asdf',
StackId: 'stack/MyStack',
StackName: 'MyStack',
Timestamp: new Date(),
LogicalResourceId: 'Xyz',
ResourceStatus: 'UPDATE_FAILED',
},
],
});
// WHEN
await deployments.rollbackStack({
stack: (0, util_1.testStack)({ stackName: 'boop' }),
validateBootstrapStackVersion: false,
force: true,
});
// THEN
expect(mock_sdk_1.mockCloudFormationClient).toHaveReceivedCommandWith(client_cloudformation_1.ContinueUpdateRollbackCommand, {
ResourcesToSkip: ['Xyz'],
StackName: 'boop',
ClientRequestToken: expect.anything(),
});
});
test('readCurrentTemplateWithNestedStacks() successfully ignores stacks without metadata', async () => {
// GIVEN
const rootSummary = stackSummaryOf('WithMetadata', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/one-resource-stack/abcd');
pushStackResourceSummaries('MetadataRoot', rootSummary);
mock_sdk_1.mockCloudFormationClient.on(client_cloudformation_1.ListStackResourcesCommand).resolves({
StackResourceSummaries: [rootSummary],
});
givenStacks({
'MetadataRoot': {
template: {
Resources: {
WithMetadata: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
},
},
},
'*': {
template: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Property: 'old-value',
},
},
},
},
},
});
const rootStack = (0, util_1.testStack)({
stackName: 'MetadataRoot',
template: {
Resources: {
WithoutMetadata: {
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Type: 'AWS::CloudFormation::Stack',
},
WithEmptyMetadata: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {},
},
WithMetadata: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
},
},
});
// WHEN
const deployedTemplate = (await deployments.readCurrentTemplateWithNestedStacks(rootStack)).deployedRootTemplate;
const nestedStacks = (await deployments.readCurrentTemplateWithNestedStacks(rootStack)).nestedStacks;
// THEN
expect(deployedTemplate).toEqual({
Resources: {
WithMetadata: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
},
});
expect(rootStack.template).toEqual({
Resources: {
WithoutMetadata: {
// Unchanged
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
},
WithEmptyMetadata: {
// Unchanged
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {},
},
WithMetadata: {
// Changed
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'https://www.magic-url.com',
},
Metadata: {
'aws:asset:path': 'one-resource-stack.nested.template.json',
},
},
},
});
expect(nestedStacks).toEqual({
WithMetadata: {
deployedTemplate: {
Resources: {
SomeResource: {
Properties: {
Property: 'old-value',
},
Type: 'AWS::Something',
},
},
},
generatedTemplate: {
Resources: {
SomeResource: {
Properties: {
Property: 'new-value',
},
Type: 'AWS::Something',
},
},
},
physicalName: 'one-resource-stack',
nestedStackTemplates: {},
},
});
});
describe('stackExists', () => {
test.each([
[false, 'deploy:here:123456789012'],
[true, 'lookup:here:123456789012'],
])('uses lookup role if requested: %p', async (tryLookupRole, expectedRoleArn) => {
const mockForEnvironment = jest.fn().mockImplementation(() => { return { sdk: new mock_sdk_1.MockSdk() }; });
sdkProvider.forEnvironment = mockForEnvironment;
givenStacks({
'*': { template: {} },
});
const result = await deployments.stackExists({
stack: (0, util_1.testStack)({
stackName: 'boop',
properties: {
assumeRoleArn: 'deploy:${AWS::Region}:${AWS::AccountId}',
lookupRole: {
arn: 'lookup:${AWS::Region}:${AWS::AccountId}',
},
},
}),
tryLookupRole,
});
expect(result).toBeTruthy();
expect(mockForEnvironment).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({
assumeRoleArn: expectedRoleArn,
}));
});
});
function pushStackResourceSummaries(stackName, ...items) {
if (!currentCfnStackResources[stackName]) {
currentCfnStackResources[stackName] = [];
}
currentCfnStackResources[stackName].push(...items);
}
function stackSummaryOf(logicalId, resourceType, physicalResourceId) {
return {
LogicalResourceId: logicalId,
PhysicalResourceId: physicalResourceId,
ResourceType: resourceType,
ResourceStatus: client_cloudformation_1.StackStatus.CREATE_COMPLETE,
LastUpdatedTimestamp: new Date(),
};
}
function givenStacks(stacks) {
jest.spyOn(deployments_1.CloudFormationStack, 'lookup').mockImplementation(async (_, stackName) => {
let stack = stacks[stackName];
if (!stack) {
stack = stacks['*'];
}
if (stack) {
const cfnStack = new fake_cloudformation_stack_1.FakeCloudformationStack({
stackName,
stackId: `stack/${stackName}`,
stackStatus: stack.stackStatus,
});
cfnStack.setTemplate(stack.template);
return cfnStack;
}
else {
return new fake_cloudformation_stack_1.FakeCloudformationStack({ stackName });
}
});
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xvdWRmb3JtYXRpb24tZGVwbG95bWVudHMudGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsb3VkZm9ybWF0aW9uLWRlcGxveW1lbnRzLnRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxJQUFJLENBQUMsSUFBSSxDQUFDLDJDQUEyQyxDQUFDLENBQUM7QUFDdkQsSUFBSSxDQUFDLElBQUksQ0FBQywrQ0FBK0MsQ0FBQyxDQUFDO0FBRTNELDBFQVF3QztBQUN4QyxvREFBMEQ7QUFDMUQsOERBQWdGO0FBQ2hGLDRFQUF3RTtBQUN4RSw0REFBOEQ7QUFDOUQsZ0VBQTREO0FBQzVELHFDQUF1QztBQUN2QyxrREFRNkI7QUFDN0IsNEVBQXVFO0FBRXZFLElBQUksV0FBNEIsQ0FBQztBQUNqQyxJQUFJLEdBQVksQ0FBQztBQUNqQixJQUFJLFdBQXdCLENBQUM7QUFDN0IsSUFBSSxxQkFBZ0MsQ0FBQztBQUNyQyxJQUFJLHdCQUFtRSxDQUFDO0FBQ3hFLFVBQVUsQ0FBQyxHQUFHLEVBQUU7SUFDZCxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUM7SUFDckIsV0FBVyxHQUFHLElBQUksMEJBQWUsRUFBRSxDQUFDO0lBQ3BDLEdBQUcsR0FBRyxJQUFJLGtCQUFPLEVBQUUsQ0FBQztJQUNwQixXQUFXLEdBQUcsSUFBSSx5QkFBVyxDQUFDLEVBQUUsV0FBVyxFQUFFLENBQUMsQ0FBQztJQUUvQyx3QkFBd0IsR0FBRyxFQUFFLENBQUM7SUFDOUIsSUFBQSxtQ0FBd0IsR0FBRSxDQUFDO0lBQzNCLDBCQUFXLENBQUMsTUFBTSxHQUFHLHFCQUFxQixHQUFHLElBQUk7U0FDOUMsRUFBRSxFQUFFO1NBQ0osaUJBQWlCLENBQUMsMEJBQVcsQ0FBQywwQkFBMEIsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDLENBQUM7SUFDbkYsSUFBQSw2QkFBa0IsR0FBRSxDQUFDO0FBQ3ZCLENBQUMsQ0FBQyxDQUFDO0FBRUgsU0FBUyxrQ0FBa0MsQ0FBQyxLQUEy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