UNPKG

aws-cdk

Version:

CDK Toolkit, the command line tool for CDK apps

1,083 lines 145 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const setup = require("./hotswap-test-setup"); const common_1 = require("../../../lib/api/hotswap/common"); const util_1 = require("../../util"); let mockUpdateLambdaCode; let mockPublishVersion; let hotswapMockSdkProvider; // TODO: more tests for parent vs child containing hotswappable changes describe.each([common_1.HotswapMode.FALL_BACK, common_1.HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { test('can hotswap a lambda function in a 1-level nested stack', async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: { Resources: { NestedStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'NestedStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, })); setup.pushNestedStackResourceSummaries('LambdaRoot', setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd')); const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: rootStack.template }); // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'my-function', S3Bucket: 'current-bucket', S3Key: 'new-key', }); }); test('hotswappable changes do not override hotswappable changes in their ancestors', async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('TwoLevelLambdaRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'TwoLevelLambdaRoot', template: { Resources: { ChildStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-one-stack-stack.nested.template.json', }, }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'ChildStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'child-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, GrandChildStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, }, }, }, })); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'GrandChildStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, })); setup.pushNestedStackResourceSummaries('TwoLevelLambdaRoot', setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd')); setup.pushNestedStackResourceSummaries('ChildStack', setup.stackSummaryOf('GrandChildStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStack/abcd')); const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'TwoLevelLambdaRoot', template: rootStack.template }); // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'child-function', S3Bucket: 'new-bucket', S3Key: 'current-key', }); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'my-function', S3Bucket: 'current-bucket', S3Key: 'new-key', }); }); test('hotswappable changes in nested stacks do not override hotswappable changes in their parent stack', async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('SiblingLambdaRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'SiblingLambdaRoot', template: { Resources: { NestedStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, }, Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'root-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'NestedStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, })); setup.pushNestedStackResourceSummaries('SiblingLambdaRoot', setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd')); rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'SiblingLambdaRoot', template: rootStack.template }); // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'root-function', S3Bucket: 'new-bucket', S3Key: 'current-key', }); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'my-function', S3Bucket: 'current-bucket', S3Key: 'new-key', }); }); test(`non-hotswappable changes in nested stacks result in a full deployment, even if their parent contains a hotswappable change in CLASSIC mode, but perform a hotswap deployment in HOTSWAP_ONLY`, async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NonHotswappableRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'NonHotswappableRoot', template: { Resources: { NestedStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, }, Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'root-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'NestedStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, PackageType: 'Image', FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, })); setup.pushNestedStackResourceSummaries('NonHotswappableRoot', setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd')); rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'NonHotswappableRoot', template: rootStack.template }); if (hotswapMode === common_1.HotswapMode.FALL_BACK) { // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); } else if (hotswapMode === common_1.HotswapMode.HOTSWAP_ONLY) { // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'root-function', S3Bucket: 'new-bucket', S3Key: 'current-key', }); } }); test(`deleting a nested stack results in a full deployment in CLASSIC mode, even if their parent contains a hotswappable change, but results in a hotswap deployment in HOTSWAP_ONLY mode`, async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackDeletionRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'NestedStackDeletionRoot', template: { Resources: { NestedStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, }, Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'root-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'NestedStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, })); setup.pushNestedStackResourceSummaries('NestedStackDeletionRoot', setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd')); rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; delete rootStack.template.Resources.NestedStack; const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'NestedStackDeletionRoot', template: rootStack.template }); if (hotswapMode === common_1.HotswapMode.FALL_BACK) { // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); } else if (hotswapMode === common_1.HotswapMode.HOTSWAP_ONLY) { // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'root-function', S3Bucket: 'new-bucket', S3Key: 'current-key', }); } }); test(`creating a nested stack results in a full deployment in CLASSIC mode, even if their parent contains a hotswappable change, but results in a hotswap deployment in HOTSWAP_ONLY mode`, async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackCreationRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'NestedStackCreationRoot', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'root-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; rootStack.template.Resources.NestedStack = { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, }; const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'NestedStackCreationRoot', template: rootStack.template }); if (hotswapMode === common_1.HotswapMode.FALL_BACK) { // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); } else if (hotswapMode === common_1.HotswapMode.HOTSWAP_ONLY) { // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'root-function', S3Bucket: 'new-bucket', S3Key: 'current-key', }); } }); test(`attempting to hotswap a newly created nested stack with the same logical ID as a resource with a different type results in a full deployment in CLASSIC mode and a hotswap deployment in HOTSWAP_ONLY mode`, async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('NestedStackTypeChangeRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'NestedStackTypeChangeRoot', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'root-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, FutureNestedStack: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'new-key', }, FunctionName: 'spooky-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); rootStack.template.Resources.Func.Properties.Code.S3Bucket = 'new-bucket'; rootStack.template.Resources.FutureNestedStack = { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-stack.nested.template.json', }, }; const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'NestedStackTypeChangeRoot', template: rootStack.template }); if (hotswapMode === common_1.HotswapMode.FALL_BACK) { // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).toBeUndefined(); expect(mockUpdateLambdaCode).not.toHaveBeenCalled(); } else if (hotswapMode === common_1.HotswapMode.HOTSWAP_ONLY) { // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'root-function', S3Bucket: 'new-bucket', S3Key: 'current-key', }); } }); test('multi-sibling + 3-layer nested stack structure is hotswappable', async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('MultiLayerRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const lambdaFunctionResource = { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, }, Metadata: { 'aws:asset:path': 'old-path', }, }; const rootStack = (0, util_1.testStack)({ stackName: 'MultiLayerRoot', template: { Resources: { ChildStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-unnamed-lambda-two-stacks-stack.nested.template.json', }, }, Func: lambdaFunctionResource, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'ChildStack', template: { Resources: { GrandChildStackA: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-unnamed-lambda-stack.nested.template.json', }, }, GrandChildStackB: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-unnamed-lambda-stack.nested.template.json', }, }, Func: lambdaFunctionResource, }, }, })); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'GrandChildStackA', template: { Resources: { Func: lambdaFunctionResource, }, }, })); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'GrandChildStackB', template: { Resources: { Func: lambdaFunctionResource, }, }, })); setup.pushNestedStackResourceSummaries('MultiLayerRoot', setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd'), setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'root-function')); setup.pushNestedStackResourceSummaries('ChildStack', setup.stackSummaryOf('GrandChildStackA', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStackA/abcd'), setup.stackSummaryOf('GrandChildStackB', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStackB/abcd'), setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'child-function')); setup.pushNestedStackResourceSummaries('GrandChildStackA', setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'grandchild-A-function')); setup.pushNestedStackResourceSummaries('GrandChildStackB', setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'grandchild-B-function')); rootStack.template.Resources.Func.Properties.Code.S3Key = 'new-key'; const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'MultiLayerRoot', template: rootStack.template }); // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'root-function', S3Bucket: 'current-bucket', S3Key: 'new-key', }); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'child-function', S3Bucket: 'current-bucket', S3Key: 'new-key', }); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'grandchild-A-function', S3Bucket: 'current-bucket', S3Key: 'new-key', }); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'grandchild-B-function', S3Bucket: 'current-bucket', S3Key: 'new-key', }); }); test('can hotswap a lambda function in a 1-level nested stack with asset parameters', async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: { Resources: { NestedStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', Parameters: { referencetoS3BucketParam: { Ref: 'S3BucketParam', }, referencetoS3KeyParam: { Ref: 'S3KeyParam', }, }, }, Metadata: { 'aws:asset:path': 'one-lambda-stack-with-asset-parameters.nested.template.json', }, }, }, Parameters: { S3BucketParam: { Type: 'String', Description: 'S3 bucket for asset', }, S3KeyParam: { Type: 'String', Description: 'S3 bucket for asset', }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'NestedStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, })); setup.pushNestedStackResourceSummaries('LambdaRoot', setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd')); const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: rootStack.template }); // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { S3BucketParam: 'bucket-param-value', S3KeyParam: 'key-param-value', }); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'my-function', S3Bucket: 'bucket-param-value', S3Key: 'key-param-value', }); }); test('can hotswap a lambda function in a 2-level nested stack with dependency on a output of 2nd level sibling stack', async () => { // GIVEN: RootStack has one child stack `FirstLevelRootStack` which further has two child stacks // `NestedLambdaStack` and `NestedSiblingStack`. `NestedLambdaStack` takes two parameters s3Key // and s3Bucket and use them for a Lambda function. // RootStack resolves s3Bucket from a root template parameter and passed to FirstLevelRootStack which // resolves s3Key through output of `NestedSiblingStack` hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('RootStack'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'RootStack', template: { Resources: { FirstLevelRootStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', Parameters: { S3BucketParam: { Ref: 'S3BucketParam', }, }, }, Metadata: { 'aws:asset:path': 'one-stack-with-two-nested-stacks-stack.template.json', }, }, }, Parameters: { S3BucketParam: { Type: 'String', Description: 'S3 bucket for asset', }, }, }, }); const firstLevelRootStack = (0, util_1.testStack)({ stackName: 'FirstLevelRootStack', template: { Resources: { NestedLambdaStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', Parameters: { referenceToS3BucketParam: { Ref: 'S3BucketParam', }, referenceToS3StackKeyOutput: { 'Fn::GetAtt': [ 'NestedSiblingStack', 'Outputs.NestedOutput', ], }, }, }, Metadata: { 'aws:asset:path': 'one-lambda-stack-with-dependency-on-sibling-stack-output.nested.template.json', }, }, NestedSiblingStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-output-stack.nested.template.json', }, }, }, Parameters: { S3BucketParam: { Type: 'String', Description: 'S3 bucket for asset', }, }, }, }); const nestedLambdaStack = (0, util_1.testStack)({ stackName: 'NestedLambdaStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, }, }, Metadata: { 'aws:asset:path': 'old-path', }, }, }); const nestedSiblingStack = (0, util_1.testStack)({ stackName: 'NestedSiblingStack', template: { Outputs: { NestedOutput: { Value: 's3-key-value-from-output' }, }, Metadata: { 'aws:asset:path': 'old-path', }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock(firstLevelRootStack); setup.addTemplateToCloudFormationLookupMock(nestedLambdaStack); setup.addTemplateToCloudFormationLookupMock(nestedSiblingStack); setup.pushNestedStackResourceSummaries('RootStack', setup.stackSummaryOf('FirstLevelRootStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/FirstLevelRootStack/abcd')); setup.pushNestedStackResourceSummaries('FirstLevelRootStack', setup.stackSummaryOf('NestedLambdaStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedLambdaStack/abcd'), setup.stackSummaryOf('NestedSiblingStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedSiblingStack/abcd')); setup.pushNestedStackResourceSummaries('NestedLambdaStack', setup.stackSummaryOf('Func', 'AWS::Lambda::Function', 'nested-lambda-function')); setup.pushNestedStackResourceSummaries('NestedSiblingStack'); const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'RootStack', template: rootStack.template }); // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { S3BucketParam: 'new-bucket', }); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'my-function', S3Bucket: 'new-bucket', S3Key: 's3-key-value-from-output', }); }); test('can hotswap a lambda function in a 1-level nested stack and read default parameters value if not provided', async () => { // GIVEN: RootStack has one child stack `NestedStack`. `NestedStack` takes two // parameters s3Key and s3Bucket and use them for a Lambda function. // RootStack resolves both parameters from root template parameters. Current/old change // has hardcoded resolved values and the new change doesn't provide parameters through // root stack forcing the evaluation of default parameter values. hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: { Resources: { NestedStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', Parameters: { referencetoS3BucketParam: { Ref: 'S3BucketParam', }, referencetoS3KeyParam: { Ref: 'S3KeyParam', }, }, }, Metadata: { 'aws:asset:path': 'one-lambda-stack-with-asset-parameters.nested.template.json', }, }, }, Parameters: { S3BucketParam: { Type: 'String', Description: 'S3 bucket for asset', Default: 'default-s3-bucket', }, S3KeyParam: { Type: 'String', Description: 'S3 bucket for asset', Default: 'default-s3-key', }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'NestedStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, })); setup.pushNestedStackResourceSummaries('LambdaRoot', setup.stackSummaryOf('NestedStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/NestedStack/abcd')); const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: rootStack.template }); // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'my-function', S3Bucket: 'default-s3-bucket', S3Key: 'default-s3-key', }); }); test('can hotswap a lambda function in a 2-level nested stack with asset parameters', async () => { // GIVEN hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, }); const rootStack = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: { Resources: { ChildStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', Parameters: { referencetoGrandChildS3BucketParam: { Ref: 'GrandChildS3BucketParam', }, referencetoGrandChildS3KeyParam: { Ref: 'GrandChildS3KeyParam', }, referencetoChildS3BucketParam: { Ref: 'ChildS3BucketParam', }, referencetoChildS3KeyParam: { Ref: 'ChildS3KeyParam', }, }, }, Metadata: { 'aws:asset:path': 'one-lambda-one-stack-stack-with-asset-parameters.nested.template.json', }, }, }, Parameters: { GrandChildS3BucketParam: { Type: 'String', Description: 'S3 bucket for asset', }, GrandChildS3KeyParam: { Type: 'String', Description: 'S3 bucket for asset', }, ChildS3BucketParam: { Type: 'String', Description: 'S3 bucket for asset', }, ChildS3KeyParam: { Type: 'String', Description: 'S3 bucket for asset', }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'ChildStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, GrandChildStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-stack-with-asset-parameters.nested.template.json', }, }, }, }, })); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'GrandChildStack', template: { Resources: { Func: { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: 'current-bucket', S3Key: 'current-key', }, FunctionName: 'my-function', }, Metadata: { 'aws:asset:path': 'old-path', }, }, }, }, })); setup.pushNestedStackResourceSummaries('LambdaRoot', setup.stackSummaryOf('ChildStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/ChildStack/abcd')); setup.pushNestedStackResourceSummaries('ChildStack', setup.stackSummaryOf('GrandChildStack', 'AWS::CloudFormation::Stack', 'arn:aws:cloudformation:bermuda-triangle-1337:123456789012:stack/GrandChildStack/abcd')); const cdkStackArtifact = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: rootStack.template }); // WHEN const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact, { GrandChildS3BucketParam: 'child-bucket-param-value', GrandChildS3KeyParam: 'child-key-param-value', ChildS3BucketParam: 'bucket-param-value', ChildS3KeyParam: 'key-param-value', }); // THEN expect(deployStackResult).not.toBeUndefined(); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'my-function', S3Bucket: 'bucket-param-value', S3Key: 'key-param-value', }); expect(mockUpdateLambdaCode).toHaveBeenCalledWith({ FunctionName: 'my-function', S3Bucket: 'child-bucket-param-value', S3Key: 'child-key-param-value', }); }); test('looking up objects in nested stacks works', async () => { hotswapMockSdkProvider = setup.setupHotswapNestedStackTests('LambdaRoot'); mockUpdateLambdaCode = jest.fn().mockReturnValue({}); mockPublishVersion = jest.fn(); hotswapMockSdkProvider.stubLambda({ updateFunctionCode: mockUpdateLambdaCode, publishVersion: mockPublishVersion, }); const rootStack = (0, util_1.testStack)({ stackName: 'LambdaRoot', template: { Resources: { NestedStack: { Type: 'AWS::CloudFormation::Stack', Properties: { TemplateURL: 'https://www.magic-url.com', }, Metadata: { 'aws:asset:path': 'one-lambda-version-stack.nested.template.json', }, }, }, }, }); setup.addTemplateToCloudFormationLookupMock(rootStack); setup.addTemplateToCloudFormationLookupMock((0, util_1.testStack)({ stackName: 'NestedStack', template: { Resources: { Func: { Type