UNPKG

aws-cdk

Version:

CDK Toolkit, the command line tool for CDK apps

1,017 lines 110 kB
"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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