From a641676a1f77dfd00e4dd252509846c45107a250 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 17 Oct 2019 10:22:28 +0300 Subject: [PATCH] fix(apigateway): deployment not invalidated when integration is changed The calculation of the deployment logical id is based on a hash of the apigateway model, which includes components such as the resource and method configurations. method integrations, which are part of the method configuration are also included, and could possible include references to other resources such as lambda functions. When the hash was calculated, tokens have been resolved using `StringConcat` which basically converted all tokens to `[Object object]`. This means, for example, that if we changed the reference of an integration lambda to a different handler, the hash would remain the same. There is no apparent reason why we can't simply resolve tokens using the CloudFormation concatenation function during "prepare", which means that if we now reference a different resource, the token will resolve to a different value and deployment will be invalidated. Fixes #4551 Fixes aws-samples/aws-cdk-intro-workshop#83 --- .../@aws-cdk/aws-apigateway/lib/deployment.ts | 13 ++--- .../aws-apigateway/test/test.deployment.ts | 53 ++++++++++++++++--- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 9a827f6038fc0..6a990812b5332 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -121,20 +121,13 @@ class LatestDeploymentResource extends CfnDeployment { * add via `addToLogicalId`. */ protected prepare() { + const stack = Stack.of(this); + // if hash components were added to the deployment, we use them to calculate // a logical ID for the deployment resource. if (this.hashComponents.length > 0) { const md5 = crypto.createHash('md5'); - this.hashComponents - .map(c => { - return Tokenization.resolve(c, { - scope: this, - resolver: new DefaultTokenResolver(new StringConcat()), - preparing: true, - }); - }) - .forEach(c => md5.update(JSON.stringify(c))); - + this.hashComponents.map(x => stack.resolve(x)).forEach(c => md5.update(JSON.stringify(c))); this.overrideLogicalId(this.originalLogicalId + md5.digest("hex")); } super.prepare(); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts index 006a88e2be8fa..c740cb2875937 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.deployment.ts @@ -1,12 +1,14 @@ import { expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; -import cdk = require('@aws-cdk/core'); +import lambda = require('@aws-cdk/aws-lambda'); +import { CfnResource, Lazy, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; +import path = require('path'); import apigateway = require('../lib'); export = { 'minimal setup'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); api.root.addMethod('GET'); @@ -57,7 +59,7 @@ export = { '"retainDeployments" can be used to control the deletion policy of the resource'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); api.root.addMethod('GET'); @@ -110,7 +112,7 @@ export = { '"description" can be set on the deployment'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); api.root.addMethod('GET'); @@ -127,7 +129,7 @@ export = { '"addToLogicalId" will "salt" the logical ID of the deployment resource'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); const deployment = new apigateway.Deployment(stack, 'deployment', { api }); api.root.addMethod('GET'); @@ -145,7 +147,7 @@ export = { // tokens supported, and are resolved upon synthesis const value = 'hello hello'; - deployment.addToLogicalId({ foo: cdk.Lazy.stringValue({ produce: () => value }) }); + deployment.addToLogicalId({ foo: Lazy.stringValue({ produce: () => value }) }); const template2 = synthesize(); test.ok(template2.Resources.deployment33381975a12dfe81474913364dc31c06e37f9449); @@ -159,12 +161,12 @@ export = { '"addDependency" can be used to add a resource as a dependency'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new Stack(); const api = new apigateway.RestApi(stack, 'api', { deploy: false, cloudWatchRole: false }); const deployment = new apigateway.Deployment(stack, 'deployment', { api }); api.root.addMethod('GET'); - const dep = new cdk.CfnResource(stack, 'MyResource', { type: 'foo' }); + const dep = new CfnResource(stack, 'MyResource', { type: 'foo' }); // WHEN deployment.node.addDependency(dep); @@ -175,4 +177,39 @@ export = { test.done(); }, + + 'integration change invalidates deployment'(test: Test) { + // GIVEN + const stack1 = new Stack(); + const stack2 = new Stack(); + const handler1 = new lambda.Function(stack1, 'handler1', { + code: lambda.Code.fromAsset(path.join(__dirname, 'integ.cors.handler')), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }); + const handler2 = new lambda.Function(stack2, 'handler2', { + code: lambda.Code.fromAsset(path.join(__dirname, 'integ.cors.handler')), + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler' + }); + + // WHEN + const api1 = new apigateway.RestApi(stack1, 'myapi', { + defaultIntegration: new apigateway.LambdaIntegration(handler1) + }); + const api2 = new apigateway.RestApi(stack2, 'myapi', { + defaultIntegration: new apigateway.LambdaIntegration(handler2) + }); + api1.root.addMethod('GET'); + api2.root.addMethod('GET'); + + // THEN + expect(stack1).to(haveResource('AWS::ApiGateway::Stage', { + DeploymentId: { Ref: 'myapiDeploymentB7EF8EB7e0b8372768854261d2d1218739e0a307' } + })); + expect(stack2).to(haveResource('AWS::ApiGateway::Stage', { + DeploymentId: { Ref: 'myapiDeploymentB7EF8EB77c517352b0f7ab73c333e36585c8f1f3' } + })); + test.done(); + } };