From 4a8e4b6b9b0656720c33c4ca042aa1ccb7c9b5ee Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Sat, 23 Feb 2019 22:18:37 +0100 Subject: [PATCH 01/23] feat(cloudformation): aws sdk js custom resource --- .../lib/aws-sdk-js-caller/.gitignore | 1 + .../lib/aws-sdk-js-caller/index.js | 52 +++++++ .../lib/aws-sdk-js-custom-resource.ts | 145 ++++++++++++++++++ .../aws-cloudformation/package-lock.json | 96 +++++++++++- .../@aws-cdk/aws-cloudformation/package.json | 6 +- 5 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/.gitignore create mode 100644 packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js create mode 100644 packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/.gitignore b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/.gitignore new file mode 100644 index 0000000000000..d4aa116a26c73 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/.gitignore @@ -0,0 +1 @@ +!*.js diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js new file mode 100644 index 0000000000000..56aa64a248a08 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js @@ -0,0 +1,52 @@ +const AWS = require('aws-sdk'); + +exports.handler = async function(event, context) { + try { + console.log(JSON.stringify(event)); + + if (event.ResourceProperties[event.RequestType]) { + const { service, action, parameters } = event.ResourceProperties[event.RequestType]; + const awsService = new AWS[service](); + await awsService[action](parameters).promise(); + } + + await respond('SUCCESS', 'OK'); + } catch (e) { + console.log(e); + await respond('FAILED', e.message); + } + + function respond(responseStatus, reason) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: reason, + PhysicalResourceId: context.logStreamName, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + NoEcho: false, + Data: {} + }); + + console.log('Responding', JSON.stringify(responseBody)); + + const parsedUrl = require('url').parse(event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: 'PUT', + headers: { 'content-type': '', 'content-length': responseBody.length } + }; + + return new Promise((resolve, reject) => { + try { + const request = require('https').request(requestOptions, resolve); + request.on('error', reject); + request.write(responseBody); + request.end(); + } catch (e) { + reject(e); + } + }); + } +} diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts new file mode 100644 index 0000000000000..417d73b6ff6b3 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts @@ -0,0 +1,145 @@ +import iam = require('@aws-cdk/aws-iam'); +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import metadata = require('aws-sdk/apis/metadata.json'); +import path = require('path'); +import { CustomResource } from './custom-resource'; + +/** + * AWS SDK service metadata. + */ +export type AwsSdkMetadata = {[key: string]: any}; + +const awsSdkMetadata: AwsSdkMetadata = metadata; + +/** + * An AWS SDK call. + */ +export interface AwsSdkCall { + /** + * The service to call + */ + service: string; + + /** + * The service action to call + */ + action: string; + + /** + * The parameters for the service action + */ + parameters: any; +} + +export interface AwsSdkJsCustomResourceProps { + /** + * The AWS SDK call to make when the resource is created. + * At least onCreate, onUpdate or onDelete must be specified. + * + * @default the call when the resource is updated + */ + onCreate?: AwsSdkCall; + + /** + * The AWS SDK call to make when the resource is updated + * + * @default the call when the resource is created + */ + onUpdate?: AwsSdkCall; + + /** + * THe AWS SDK call to make when the resource is deleted + */ + onDelete?: AwsSdkCall; + + /** + * The IAM policy statements to allow the different calls. Use only if + * resource restriction is needed. + * + * @default Allow onCreate, onUpdate and onDelete calls on all resources ('*') + */ + policyStatements?: iam.PolicyStatement[]; +} + +export class AwsSdkJsCustomResource extends cdk.Construct { + /** + * The AWS SDK call made when the resource is created. + */ + public readonly onCreate?: AwsSdkCall; + + /** + * The AWS SDK call made when the resource is udpated. + */ + public readonly onUpdate?: AwsSdkCall; + + /** + * The AWS SDK call made when the resource is deleted. + */ + public readonly onDelete?: AwsSdkCall; + + /** + * The IAM policy statements used by the lambda provider. + */ + public readonly policyStatements: iam.PolicyStatement[]; + + constructor(scope: cdk.Construct, id: string, props: AwsSdkJsCustomResourceProps) { + super(scope, id); + + if (!props.onCreate && !props.onUpdate && !props.onDelete) { + throw new Error('At least `onCreate`, `onUpdate` or `onDelete` must be specified.'); + } + + this.onCreate = props.onCreate || props.onUpdate; + this.onUpdate = props.onUpdate || props.onCreate; + this.onDelete = props.onDelete; + + const fn = new lambda.SingletonFunction(this, 'Function', { + code: lambda.Code.asset(path.join(__dirname, 'aws-sdk-js-caller')), + runtime: lambda.Runtime.NodeJS810, + handler: 'index.handler', + uuid: '679f53fa-c002-430c-b0da-5b7982bd2287' + }); + + if (props.policyStatements) { + props.policyStatements.forEach(statement => { + fn.addToRolePolicy(statement); + }); + this.policyStatements = props.policyStatements; + } else { // Derive statements from AWS SDK calls + this.policyStatements = []; + + [this.onCreate, this.onUpdate, this.onDelete].forEach(call => { + if (call) { + const statement = new iam.PolicyStatement() + .addAction(awsSdkToIamAction(call.service, call.action)) + .addAllResources(); + fn.addToRolePolicy(statement); // TODO: remove duplicates? + this.policyStatements.push(statement); + } + }); + } + + new CustomResource(this, 'Resource', { + lambdaProvider: fn, + properties: { + create: this.onCreate, + update: this.onUpdate, + delete: this.onDelete + } + }); + } +} + +/** + * Transform SDK service/action to IAM action using metadata from aws-sdk module. + * Example: CloudWatchLogs with putRetentionPolicy => logs:PutRetentionPolicy + * + * TODO: is this mapping correct for all services? + */ +function awsSdkToIamAction(service: string, action: string): string { + const srv = service.toLowerCase(); + const iamService = awsSdkMetadata[srv].prefix || srv; + const iamAction = action.charAt(0).toUpperCase() + action.slice(1); + return `${iamService}:${iamAction}`; +} diff --git a/packages/@aws-cdk/aws-cloudformation/package-lock.json b/packages/@aws-cdk/aws-cloudformation/package-lock.json index a1173ece57cf8..ca41b792aeb7d 100644 --- a/packages/@aws-cdk/aws-cloudformation/package-lock.json +++ b/packages/@aws-cdk/aws-cloudformation/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/aws-cloudformation", - "version": "0.23.0", + "version": "0.24.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -10,11 +10,105 @@ "integrity": "sha512-jQ21kQ120mo+IrDs1nFNVm/AsdFxIx2+vZ347DbogHJPd/JzKNMOqU6HCYin1W6v8l5R9XSO2/e9cxmn7HAnVw==", "dev": true }, + "aws-sdk": { + "version": "2.409.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.409.0.tgz", + "integrity": "sha512-QV6j9zBQq/Kz8BqVOrJ03ABjMKtErXdUT1YdYEljoLQZimpzt0ZdQwJAsoZIsxxriOJgrqeZsQUklv9AFQaldQ==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" } } } diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index 50b80d89ce878..0f2bf71b1dfc7 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -73,8 +73,12 @@ "@aws-cdk/aws-iam": "^0.24.1", "@aws-cdk/aws-lambda": "^0.24.1", "@aws-cdk/aws-sns": "^0.24.1", - "@aws-cdk/cdk": "^0.24.1" + "@aws-cdk/cdk": "^0.24.1", + "aws-sdk": "^2.409.0" }, + "bundledDependencies": [ + "aws-sdk" + ], "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { "@aws-cdk/aws-codepipeline-api": "^0.24.1", From 5ab6588929c91aec056fbc96f59b4cf7c23154b9 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 25 Feb 2019 14:18:06 +0100 Subject: [PATCH 02/23] Log AWS SDK version --- .../@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js index 56aa64a248a08..ab03509f23277 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js @@ -3,6 +3,7 @@ const AWS = require('aws-sdk'); exports.handler = async function(event, context) { try { console.log(JSON.stringify(event)); + console.log('AWS SDK VERSION: ' + AWS.VERSION); if (event.ResourceProperties[event.RequestType]) { const { service, action, parameters } = event.ResourceProperties[event.RequestType]; From 15a6ef8486694273a42d752cd80b1c80e449c40a Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 25 Feb 2019 14:25:42 +0100 Subject: [PATCH 03/23] Add tests --- .../@aws-cdk/aws-cloudformation/lib/index.ts | 1 + ...g.aws-sdk-js-custom-resource.expected.json | 160 ++++++++++++++++ .../test/integ.aws-sdk-js-custom-resource.ts | 23 +++ .../test/test.aws-sdk-js-custom-resource.ts | 176 ++++++++++++++++++ 4 files changed, 360 insertions(+) create mode 100644 packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json create mode 100644 packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts create mode 100644 packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts diff --git a/packages/@aws-cdk/aws-cloudformation/lib/index.ts b/packages/@aws-cdk/aws-cloudformation/lib/index.ts index 2a9dc1178f2fe..6a8898f7f939e 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/index.ts @@ -1,5 +1,6 @@ export * from './custom-resource'; export * from './pipeline-actions'; +export * from './aws-sdk-js-custom-resource'; // AWS::CloudFormation CloudFormation Resources: export * from './cloudformation.generated'; diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json new file mode 100644 index 0000000000000..cd8aa5f7da5f4 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json @@ -0,0 +1,160 @@ +{ + "Resources": { + "TopicBFC7AF6E": { + "Type": "AWS::SNS::Topic" + }, + "AwsSdkE966FE43": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonLambda679f53fac002430cb0da5b7982bd2287AF41E197", + "Arn" + ] + }, + "Create": { + "service": "SNS", + "action": "publish", + "parameters": { + "Message": "hello", + "TopicArn": { + "Ref": "TopicBFC7AF6E" + } + } + }, + "Update": { + "service": "SNS", + "action": "publish", + "parameters": { + "Message": "hello", + "TopicArn": { + "Ref": "TopicBFC7AF6E" + } + } + } + } + }, + "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicy4215D22C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicy4215D22C", + "Roles": [ + { + "Ref": "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98" + } + ] + } + }, + "SingletonLambda679f53fac002430cb0da5b7982bd2287AF41E197": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3Bucket277B9EBE" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3VersionKey7868E97C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3VersionKey7868E97C" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicy4215D22C", + "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98" + ] + } + }, + "Parameters": { + "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3Bucket277B9EBE": { + "Type": "String", + "Description": "S3 bucket for asset \"aws-cdk-sdk-js/SingletonLambda679f53fac002430cb0da5b7982bd2287/Code\"" + }, + "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3VersionKey7868E97C": { + "Type": "String", + "Description": "S3 key for asset version \"aws-cdk-sdk-js/SingletonLambda679f53fac002430cb0da5b7982bd2287/Code\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts new file mode 100644 index 0000000000000..c1d816a6abfec --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); +import { AwsSdkJsCustomResource } from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-sdk-js'); + +const topic = new sns.Topic(stack, 'Topic'); + +new AwsSdkJsCustomResource(stack, 'AwsSdk', { + onUpdate: { + service: 'SNS', + action: 'publish', + parameters: { + Message: 'hello', + TopicArn: topic.topicArn, + } + } +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts new file mode 100644 index 0000000000000..8794b25738239 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts @@ -0,0 +1,176 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import { AwsSdkJsCustomResource } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'aws sdk js custom resource with onCreate and onDelete'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new AwsSdkJsCustomResource(stack, 'AwsSdk', { + onCreate: { + service: 'CloudWatchLogs', + action: 'putRetentionPolicy', + parameters: { + logGroupName: '/aws/lambda/loggroup', + retentionInDays: 90 + } + }, + onDelete: { + service: 'CloudWatchLogs', + action: 'deleteRetentionPolicy', + parameters: { + logGroupName: '/aws/lambda/loggroup', + } + } + }); + + // THEN + expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { + "Create": { + "service": "CloudWatchLogs", + "action": "putRetentionPolicy", + "parameters": { + "logGroupName": "/aws/lambda/loggroup", + "retentionInDays": 90 + } + }, + "Update": { + "service": "CloudWatchLogs", + "action": "putRetentionPolicy", + "parameters": { + "logGroupName": "/aws/lambda/loggroup", + "retentionInDays": 90 + } + }, + "Delete": { + "service": "CloudWatchLogs", + "action": "deleteRetentionPolicy", + "parameters": { + "logGroupName": "/aws/lambda/loggroup" + } + } + })); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "logs:PutRetentionPolicy", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "logs:PutRetentionPolicy", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "logs:DeleteRetentionPolicy", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + })); + + test.done(); + }, + + 'onCreate defaults to onUpdate'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new AwsSdkJsCustomResource(stack, 'AwsSdk', { + onUpdate: { + service: 's3', + action: 'putObject', + parameters: { + Bucket: 'my-bucket', + Key: 'my-key', + Body: 'my-body' + } + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { + "Create": { + "service": "s3", + "action": "putObject", + "parameters": { + "Bucket": "my-bucket", + "Key": "my-key", + "Body": "my-body" + } + }, + "Update": { + "service": "s3", + "action": "putObject", + "parameters": { + "Bucket": "my-bucket", + "Key": "my-key", + "Body": "my-body" + } + }, + })); + + test.done(); + }, + + 'with custom policyStatements'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new AwsSdkJsCustomResource(stack, 'AwsSdk', { + onUpdate: { + service: 'S3', + action: 'putObject', + parameters: { + Bucket: 'my-bucket', + Key: 'my-key', + Body: 'my-body' + } + }, + policyStatements: [ + new iam.PolicyStatement() + .addAction('s3:PutObject') + .addResource('arn:aws:s3:::my-bucket/my-key') + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": "arn:aws:s3:::my-bucket/my-key" + }, + ], + "Version": "2012-10-17" + }, + })); + + test.done(); + }, + + 'fails when no calls are specified'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new AwsSdkJsCustomResource(stack, 'AwsSdk', {}); + }, /`onCreate`.+`onUpdate`.+`onDelete`/); + + test.done(); + } +}; From ad864577187a04cdbd7605c45ac8585802c11399 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 4 Mar 2019 10:59:08 +0100 Subject: [PATCH 04/23] Return API response as output attributes --- .../lib/aws-sdk-js-caller/index.js | 35 ++++++++++--- .../lib/aws-sdk-js-custom-resource.ts | 19 ++++++- ...g.aws-sdk-js-custom-resource.expected.json | 49 ++++++++++++++++++- .../test/integ.aws-sdk-js-custom-resource.ts | 14 +++++- 4 files changed, 105 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js index ab03509f23277..a7dece2a6e872 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js @@ -1,32 +1,53 @@ const AWS = require('aws-sdk'); +/** + * Flattens a nested object. Keys are path. + */ +function flatten(object) { + return Object.assign( + {}, + ...function _flatten(child, path = []) { + return [].concat(...Object.keys(child).map(key => typeof child[key] === 'object' + ? _flatten(child[key], path.concat([key])) + : ({ [path.concat([key]).join('.')] : child[key] }) + )); + }(object) + ); +} + exports.handler = async function(event, context) { try { console.log(JSON.stringify(event)); console.log('AWS SDK VERSION: ' + AWS.VERSION); + let data = {}; + if (event.ResourceProperties[event.RequestType]) { - const { service, action, parameters } = event.ResourceProperties[event.RequestType]; + const { service, action, parameters = {} } = event.ResourceProperties[event.RequestType]; + const awsService = new AWS[service](); - await awsService[action](parameters).promise(); + + const response = await awsService[action](parameters).promise(); + + data = flatten(response); } - await respond('SUCCESS', 'OK'); + await respond('SUCCESS', 'OK', event.LogicalResourceId, data); } catch (e) { console.log(e); - await respond('FAILED', e.message); + await respond('FAILED', e.message, context.logStreamName, {}); } - function respond(responseStatus, reason) { + function respond(responseStatus, reason, physicalResourceId, data) { const responseBody = JSON.stringify({ Status: responseStatus, Reason: reason, - PhysicalResourceId: context.logStreamName, + PhysicalResourceId: physicalResourceId, StackId: event.StackId, RequestId: event.RequestId, LogicalResourceId: event.LogicalResourceId, NoEcho: false, - Data: {} + Data: data }); console.log('Responding', JSON.stringify(responseBody)); diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts index 417d73b6ff6b3..7fb613f749a20 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts @@ -29,7 +29,7 @@ export interface AwsSdkCall { /** * The parameters for the service action */ - parameters: any; + parameters?: any; } export interface AwsSdkJsCustomResourceProps { @@ -83,6 +83,11 @@ export class AwsSdkJsCustomResource extends cdk.Construct { */ public readonly policyStatements: iam.PolicyStatement[]; + /** + * The custom resource making the AWS SDK calls. + */ + private readonly customResource: CustomResource; + constructor(scope: cdk.Construct, id: string, props: AwsSdkJsCustomResourceProps) { super(scope, id); @@ -120,7 +125,7 @@ export class AwsSdkJsCustomResource extends cdk.Construct { }); } - new CustomResource(this, 'Resource', { + this.customResource = new CustomResource(this, 'Resource', { lambdaProvider: fn, properties: { create: this.onCreate, @@ -129,6 +134,16 @@ export class AwsSdkJsCustomResource extends cdk.Construct { } }); } + + /** + * Returns response data for the AWS SDK call. + * Example for S3 / listBucket : 'Buckets.0.Name' + * + * @param dataPath the path to the data + */ + public getData(dataPath: string) { + return this.customResource.getAtt(dataPath); + } } /** diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json index cd8aa5f7da5f4..fb8c20258b738 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json @@ -3,7 +3,7 @@ "TopicBFC7AF6E": { "Type": "AWS::SNS::Topic" }, - "AwsSdkE966FE43": { + "Publish2E9BDF73": { "Type": "AWS::CloudFormation::CustomResource", "Properties": { "ServiceToken": { @@ -79,6 +79,16 @@ "Action": "sns:Publish", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "sns:ListTopics", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "sns:ListTopics", + "Effect": "Allow", + "Resource": "*" } ], "Version": "2012-10-17" @@ -145,6 +155,25 @@ "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicy4215D22C", "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98" ] + }, + "ListTopicsCE1E0341": { + "Type": "AWS::CloudFormation::CustomResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonLambda679f53fac002430cb0da5b7982bd2287AF41E197", + "Arn" + ] + }, + "Create": { + "service": "SNS", + "action": "listTopics" + }, + "Update": { + "service": "SNS", + "action": "listTopics" + } + } } }, "Parameters": { @@ -156,5 +185,23 @@ "Type": "String", "Description": "S3 key for asset version \"aws-cdk-sdk-js/SingletonLambda679f53fac002430cb0da5b7982bd2287/Code\"" } + }, + "Outputs": { + "MessageId": { + "Value": { + "Fn::GetAtt": [ + "Publish2E9BDF73", + "MessageId" + ] + } + }, + "TopicArn": { + "Value": { + "Fn::GetAtt": [ + "ListTopicsCE1E0341", + "Topics.0.TopicArn" + ] + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts index c1d816a6abfec..792682629d0b4 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts @@ -9,15 +9,25 @@ const stack = new cdk.Stack(app, 'aws-cdk-sdk-js'); const topic = new sns.Topic(stack, 'Topic'); -new AwsSdkJsCustomResource(stack, 'AwsSdk', { +const snsPublish = new AwsSdkJsCustomResource(stack, 'Publish', { onUpdate: { service: 'SNS', action: 'publish', parameters: { Message: 'hello', - TopicArn: topic.topicArn, + TopicArn: topic.topicArn } } }); +const listTopics = new AwsSdkJsCustomResource(stack, 'ListTopics', { + onUpdate: { + service: 'SNS', + action: 'listTopics' + } +}); + +new cdk.Output(stack, 'MessageId', { value: snsPublish.getData('MessageId') }); +new cdk.Output(stack, 'TopicArn', { value: listTopics.getData('Topics.0.TopicArn') }); + app.run(); From 84c73ab5a0ab9ab21fbb31553768f9a4dace7276 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 4 Mar 2019 11:33:10 +0100 Subject: [PATCH 05/23] Remove default on onUpdate --- .../lib/aws-sdk-js-custom-resource.ts | 6 ++++-- .../test/test.aws-sdk-js-custom-resource.ts | 13 ------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts index 7fb613f749a20..ca616171ed94b 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts @@ -44,12 +44,14 @@ export interface AwsSdkJsCustomResourceProps { /** * The AWS SDK call to make when the resource is updated * - * @default the call when the resource is created + * @default no call */ onUpdate?: AwsSdkCall; /** * THe AWS SDK call to make when the resource is deleted + * + * @default no call */ onDelete?: AwsSdkCall; @@ -96,7 +98,7 @@ export class AwsSdkJsCustomResource extends cdk.Construct { } this.onCreate = props.onCreate || props.onUpdate; - this.onUpdate = props.onUpdate || props.onCreate; + this.onUpdate = props.onUpdate; this.onDelete = props.onDelete; const fn = new lambda.SingletonFunction(this, 'Function', { diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts index 8794b25738239..59664c995418b 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts @@ -40,14 +40,6 @@ export = { "retentionInDays": 90 } }, - "Update": { - "service": "CloudWatchLogs", - "action": "putRetentionPolicy", - "parameters": { - "logGroupName": "/aws/lambda/loggroup", - "retentionInDays": 90 - } - }, "Delete": { "service": "CloudWatchLogs", "action": "deleteRetentionPolicy", @@ -60,11 +52,6 @@ export = { expect(stack).to(haveResource('AWS::IAM::Policy', { "PolicyDocument": { "Statement": [ - { - "Action": "logs:PutRetentionPolicy", - "Effect": "Allow", - "Resource": "*" - }, { "Action": "logs:PutRetentionPolicy", "Effect": "Allow", From 5ba77657285c118a330eea6d3c28d684d51792c6 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 4 Mar 2019 18:11:16 +0100 Subject: [PATCH 06/23] Add options to specify physical resource id --- .../lib/aws-sdk-js-caller/index.js | 4 +++- .../lib/aws-sdk-js-custom-resource.ts | 12 ++++++++++++ .../integ.aws-sdk-js-custom-resource.expected.json | 6 ++++++ .../test/integ.aws-sdk-js-custom-resource.ts | 2 ++ .../test/test.aws-sdk-js-custom-resource.ts | 7 ++++++- 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js index a7dece2a6e872..08f04c67fe8e5 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js @@ -22,6 +22,8 @@ exports.handler = async function(event, context) { let data = {}; + const physicalResourceId = event.ResourceProperties.PhysicalResourceId; + if (event.ResourceProperties[event.RequestType]) { const { service, action, parameters = {} } = event.ResourceProperties[event.RequestType]; @@ -32,7 +34,7 @@ exports.handler = async function(event, context) { data = flatten(response); } - await respond('SUCCESS', 'OK', event.LogicalResourceId, data); + await respond('SUCCESS', 'OK', physicalResourceId, data); } catch (e) { console.log(e); await respond('FAILED', e.message, context.logStreamName, {}); diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts index ca616171ed94b..9eb06024016c3 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts @@ -33,6 +33,11 @@ export interface AwsSdkCall { } export interface AwsSdkJsCustomResourceProps { + /** + * The physical resource id of the custom resource. + */ + physicalResourceId: string; + /** * The AWS SDK call to make when the resource is created. * At least onCreate, onUpdate or onDelete must be specified. @@ -65,6 +70,11 @@ export interface AwsSdkJsCustomResourceProps { } export class AwsSdkJsCustomResource extends cdk.Construct { + /** + * The physical resource id of the custom resource. + */ + public readonly physicalResourceId: string; + /** * The AWS SDK call made when the resource is created. */ @@ -100,6 +110,7 @@ export class AwsSdkJsCustomResource extends cdk.Construct { this.onCreate = props.onCreate || props.onUpdate; this.onUpdate = props.onUpdate; this.onDelete = props.onDelete; + this.physicalResourceId = props.physicalResourceId; const fn = new lambda.SingletonFunction(this, 'Function', { code: lambda.Code.asset(path.join(__dirname, 'aws-sdk-js-caller')), @@ -130,6 +141,7 @@ export class AwsSdkJsCustomResource extends cdk.Construct { this.customResource = new CustomResource(this, 'Resource', { lambdaProvider: fn, properties: { + physicalResourceId: this.physicalResourceId, create: this.onCreate, update: this.onUpdate, delete: this.onDelete diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json index fb8c20258b738..e89088b6857b8 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json @@ -12,6 +12,9 @@ "Arn" ] }, + "PhysicalResourceId": { + "Ref": "TopicBFC7AF6E" + }, "Create": { "service": "SNS", "action": "publish", @@ -165,6 +168,9 @@ "Arn" ] }, + "PhysicalResourceId": { + "Ref": "TopicBFC7AF6E" + }, "Create": { "service": "SNS", "action": "listTopics" diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts index 792682629d0b4..8ccb7bafdb617 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts @@ -10,6 +10,7 @@ const stack = new cdk.Stack(app, 'aws-cdk-sdk-js'); const topic = new sns.Topic(stack, 'Topic'); const snsPublish = new AwsSdkJsCustomResource(stack, 'Publish', { + physicalResourceId: topic.topicArn, onUpdate: { service: 'SNS', action: 'publish', @@ -21,6 +22,7 @@ const snsPublish = new AwsSdkJsCustomResource(stack, 'Publish', { }); const listTopics = new AwsSdkJsCustomResource(stack, 'ListTopics', { + physicalResourceId: topic.topicArn, onUpdate: { service: 'SNS', action: 'listTopics' diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts index 59664c995418b..5c7329c887f7f 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts @@ -13,6 +13,7 @@ export = { // WHEN new AwsSdkJsCustomResource(stack, 'AwsSdk', { + physicalResourceId: 'id1234', onCreate: { service: 'CloudWatchLogs', action: 'putRetentionPolicy', @@ -32,6 +33,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { + "PhysicalResourceId": "id1234", "Create": { "service": "CloudWatchLogs", "action": "putRetentionPolicy", @@ -76,6 +78,7 @@ export = { // WHEN new AwsSdkJsCustomResource(stack, 'AwsSdk', { + physicalResourceId: 'id1234', onUpdate: { service: 's3', action: 'putObject', @@ -89,6 +92,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { + "PhysicalResourceId": "id1234", "Create": { "service": "s3", "action": "putObject", @@ -118,6 +122,7 @@ export = { // WHEN new AwsSdkJsCustomResource(stack, 'AwsSdk', { + physicalResourceId: 'id1234', onUpdate: { service: 'S3', action: 'putObject', @@ -155,7 +160,7 @@ export = { const stack = new cdk.Stack(); test.throws(() => { - new AwsSdkJsCustomResource(stack, 'AwsSdk', {}); + new AwsSdkJsCustomResource(stack, 'AwsSdk', { physicalResourceId: 'id1234' }); }, /`onCreate`.+`onUpdate`.+`onDelete`/); test.done(); From 5811c94efda885330b38380156dd2a258a7e5c81 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 26 Mar 2019 17:01:45 +0100 Subject: [PATCH 07/23] Rename to AwsCustomResource --- .../.gitignore | 0 .../index.js | 0 ...custom-resource.ts => aws-custom-resource.ts} | 9 +++++---- .../@aws-cdk/aws-cloudformation/lib/index.ts | 2 +- .../aws-cloudformation/package-lock.json | 2 +- ...n => integ.aws-custom-resource.expected.json} | 16 +++++++++++++--- ...-resource.ts => integ.aws-custom-resource.ts} | 10 +++++----- ...m-resource.ts => test.aws-custom-resource.ts} | 14 +++++++------- 8 files changed, 32 insertions(+), 21 deletions(-) rename packages/@aws-cdk/aws-cloudformation/lib/{aws-sdk-js-caller => aws-custom-resource-provider}/.gitignore (100%) rename packages/@aws-cdk/aws-cloudformation/lib/{aws-sdk-js-caller => aws-custom-resource-provider}/index.js (100%) rename packages/@aws-cdk/aws-cloudformation/lib/{aws-sdk-js-custom-resource.ts => aws-custom-resource.ts} (93%) rename packages/@aws-cdk/aws-cloudformation/test/{integ.aws-sdk-js-custom-resource.expected.json => integ.aws-custom-resource.expected.json} (94%) rename packages/@aws-cdk/aws-cloudformation/test/{integ.aws-sdk-js-custom-resource.ts => integ.aws-custom-resource.ts} (60%) rename packages/@aws-cdk/aws-cloudformation/test/{test.aws-sdk-js-custom-resource.ts => test.aws-custom-resource.ts} (89%) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/.gitignore b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/.gitignore similarity index 100% rename from packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/.gitignore rename to packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/.gitignore diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.js similarity index 100% rename from packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-caller/index.js rename to packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.js diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts similarity index 93% rename from packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts rename to packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index 9eb06024016c3..4fe68a8b2d526 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -32,7 +32,7 @@ export interface AwsSdkCall { parameters?: any; } -export interface AwsSdkJsCustomResourceProps { +export interface AwsCustomResourceProps { /** * The physical resource id of the custom resource. */ @@ -69,7 +69,7 @@ export interface AwsSdkJsCustomResourceProps { policyStatements?: iam.PolicyStatement[]; } -export class AwsSdkJsCustomResource extends cdk.Construct { +export class AwsCustomResource extends cdk.Construct { /** * The physical resource id of the custom resource. */ @@ -100,7 +100,7 @@ export class AwsSdkJsCustomResource extends cdk.Construct { */ private readonly customResource: CustomResource; - constructor(scope: cdk.Construct, id: string, props: AwsSdkJsCustomResourceProps) { + constructor(scope: cdk.Construct, id: string, props: AwsCustomResourceProps) { super(scope, id); if (!props.onCreate && !props.onUpdate && !props.onDelete) { @@ -113,7 +113,7 @@ export class AwsSdkJsCustomResource extends cdk.Construct { this.physicalResourceId = props.physicalResourceId; const fn = new lambda.SingletonFunction(this, 'Function', { - code: lambda.Code.asset(path.join(__dirname, 'aws-sdk-js-caller')), + code: lambda.Code.asset(path.join(__dirname, 'aws-custom-resource-provider')), runtime: lambda.Runtime.NodeJS810, handler: 'index.handler', uuid: '679f53fa-c002-430c-b0da-5b7982bd2287' @@ -139,6 +139,7 @@ export class AwsSdkJsCustomResource extends cdk.Construct { } this.customResource = new CustomResource(this, 'Resource', { + resourceType: 'Custom::AWS', lambdaProvider: fn, properties: { physicalResourceId: this.physicalResourceId, diff --git a/packages/@aws-cdk/aws-cloudformation/lib/index.ts b/packages/@aws-cdk/aws-cloudformation/lib/index.ts index 6a8898f7f939e..7fd6c83cfd0b9 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/index.ts @@ -1,6 +1,6 @@ export * from './custom-resource'; export * from './pipeline-actions'; -export * from './aws-sdk-js-custom-resource'; +export * from './aws-custom-resource'; // AWS::CloudFormation CloudFormation Resources: export * from './cloudformation.generated'; diff --git a/packages/@aws-cdk/aws-cloudformation/package-lock.json b/packages/@aws-cdk/aws-cloudformation/package-lock.json index d144647e00268..5245173fd1bea 100644 --- a/packages/@aws-cdk/aws-cloudformation/package-lock.json +++ b/packages/@aws-cdk/aws-cloudformation/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/aws-cloudformation", - "version": "0.25.3", + "version": "0.26.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json similarity index 94% rename from packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json rename to packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json index e89088b6857b8..4527d5d0c4619 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json @@ -4,7 +4,7 @@ "Type": "AWS::SNS::Topic" }, "Publish2E9BDF73": { - "Type": "AWS::CloudFormation::CustomResource", + "Type": "Custom::AWS", "Properties": { "ServiceToken": { "Fn::GetAtt": [ @@ -46,7 +46,17 @@ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { - "Service": "lambda.amazonaws.com" + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } } } ], @@ -160,7 +170,7 @@ ] }, "ListTopicsCE1E0341": { - "Type": "AWS::CloudFormation::CustomResource", + "Type": "Custom::AWS", "Properties": { "ServiceToken": { "Fn::GetAtt": [ diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts similarity index 60% rename from packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts rename to packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts index 8ccb7bafdb617..ffb83a4a56b9a 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import sns = require('@aws-cdk/aws-sns'); import cdk = require('@aws-cdk/cdk'); -import { AwsSdkJsCustomResource } from '../lib'; +import { AwsCustomResource } from '../lib'; const app = new cdk.App(); @@ -9,7 +9,7 @@ const stack = new cdk.Stack(app, 'aws-cdk-sdk-js'); const topic = new sns.Topic(stack, 'Topic'); -const snsPublish = new AwsSdkJsCustomResource(stack, 'Publish', { +const snsPublish = new AwsCustomResource(stack, 'Publish', { physicalResourceId: topic.topicArn, onUpdate: { service: 'SNS', @@ -21,7 +21,7 @@ const snsPublish = new AwsSdkJsCustomResource(stack, 'Publish', { } }); -const listTopics = new AwsSdkJsCustomResource(stack, 'ListTopics', { +const listTopics = new AwsCustomResource(stack, 'ListTopics', { physicalResourceId: topic.topicArn, onUpdate: { service: 'SNS', @@ -29,7 +29,7 @@ const listTopics = new AwsSdkJsCustomResource(stack, 'ListTopics', { } }); -new cdk.Output(stack, 'MessageId', { value: snsPublish.getData('MessageId') }); -new cdk.Output(stack, 'TopicArn', { value: listTopics.getData('Topics.0.TopicArn') }); +new cdk.CfnOutput(stack, 'MessageId', { value: snsPublish.getData('MessageId') }); +new cdk.CfnOutput(stack, 'TopicArn', { value: listTopics.getData('Topics.0.TopicArn') }); app.run(); diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource.ts similarity index 89% rename from packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts rename to packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource.ts index 5c7329c887f7f..3ccb1270300af 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.aws-sdk-js-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource.ts @@ -2,7 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; -import { AwsSdkJsCustomResource } from '../lib'; +import { AwsCustomResource } from '../lib'; // tslint:disable:object-literal-key-quotes @@ -12,7 +12,7 @@ export = { const stack = new cdk.Stack(); // WHEN - new AwsSdkJsCustomResource(stack, 'AwsSdk', { + new AwsCustomResource(stack, 'AwsSdk', { physicalResourceId: 'id1234', onCreate: { service: 'CloudWatchLogs', @@ -32,7 +32,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { + expect(stack).to(haveResource('Custom::AWS', { "PhysicalResourceId": "id1234", "Create": { "service": "CloudWatchLogs", @@ -77,7 +77,7 @@ export = { const stack = new cdk.Stack(); // WHEN - new AwsSdkJsCustomResource(stack, 'AwsSdk', { + new AwsCustomResource(stack, 'AwsSdk', { physicalResourceId: 'id1234', onUpdate: { service: 's3', @@ -91,7 +91,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::CloudFormation::CustomResource', { + expect(stack).to(haveResource('Custom::AWS', { "PhysicalResourceId": "id1234", "Create": { "service": "s3", @@ -121,7 +121,7 @@ export = { const stack = new cdk.Stack(); // WHEN - new AwsSdkJsCustomResource(stack, 'AwsSdk', { + new AwsCustomResource(stack, 'AwsSdk', { physicalResourceId: 'id1234', onUpdate: { service: 'S3', @@ -160,7 +160,7 @@ export = { const stack = new cdk.Stack(); test.throws(() => { - new AwsSdkJsCustomResource(stack, 'AwsSdk', { physicalResourceId: 'id1234' }); + new AwsCustomResource(stack, 'AwsSdk', { physicalResourceId: 'id1234' }); }, /`onCreate`.+`onUpdate`.+`onDelete`/); test.done(); From 941913b2f5359e777c8d52d94f491a0ebc469fe5 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 28 Mar 2019 09:36:25 +0100 Subject: [PATCH 08/23] Add readonly --- .../lib/aws-custom-resource.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index 4fe68a8b2d526..0a8912d066a8e 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -19,24 +19,24 @@ export interface AwsSdkCall { /** * The service to call */ - service: string; + readonly service: string; /** * The service action to call */ - action: string; + readonly action: string; /** * The parameters for the service action */ - parameters?: any; + readonly parameters?: any; } export interface AwsCustomResourceProps { /** * The physical resource id of the custom resource. */ - physicalResourceId: string; + readonly physicalResourceId: string; /** * The AWS SDK call to make when the resource is created. @@ -44,21 +44,21 @@ export interface AwsCustomResourceProps { * * @default the call when the resource is updated */ - onCreate?: AwsSdkCall; + readonly onCreate?: AwsSdkCall; /** * The AWS SDK call to make when the resource is updated * * @default no call */ - onUpdate?: AwsSdkCall; + readonly onUpdate?: AwsSdkCall; /** * THe AWS SDK call to make when the resource is deleted * * @default no call */ - onDelete?: AwsSdkCall; + readonly onDelete?: AwsSdkCall; /** * The IAM policy statements to allow the different calls. Use only if @@ -66,7 +66,7 @@ export interface AwsCustomResourceProps { * * @default Allow onCreate, onUpdate and onDelete calls on all resources ('*') */ - policyStatements?: iam.PolicyStatement[]; + readonly policyStatements?: iam.PolicyStatement[]; } export class AwsCustomResource extends cdk.Construct { From ae446b955d5a6e62f4070c3bea4e419f282f3924 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 28 Mar 2019 15:11:00 +0100 Subject: [PATCH 09/23] Write provider in ts --- .../aws-custom-resource-provider/.gitignore | 1 - .../{index.js => index.ts} | 30 +++++++++++-------- .../aws-cloudformation/package-lock.json | 6 ++++ .../@aws-cdk/aws-cloudformation/package.json | 1 + 4 files changed, 25 insertions(+), 13 deletions(-) delete mode 100644 packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/.gitignore rename packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/{index.js => index.ts} (63%) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/.gitignore b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/.gitignore deleted file mode 100644 index d4aa116a26c73..0000000000000 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!*.js diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.js b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts similarity index 63% rename from packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.js rename to packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts index 08f04c67fe8e5..669156b9de45e 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.js +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts @@ -1,24 +1,30 @@ -const AWS = require('aws-sdk'); +// tslint:disable:no-console +import AWS = require('aws-sdk'); /** - * Flattens a nested object. Keys are path. + * Flattens a nested object + * + * @param object the object to be flattened + * @returns a flat object with path as keys */ -function flatten(object) { +function flatten(object: object): object { return Object.assign( {}, - ...function _flatten(child, path = []) { - return [].concat(...Object.keys(child).map(key => typeof child[key] === 'object' - ? _flatten(child[key], path.concat([key])) - : ({ [path.concat([key]).join('.')] : child[key] }) + ...function _flatten(child: any, path: string[] = []): any { + return [].concat(...Object.keys(child) + .map(key => + typeof child[key] === 'object' + ? _flatten(child[key], path.concat([key])) + : ({ [path.concat([key]).join('.')]: child[key] }) )); }(object) ); } -exports.handler = async function(event, context) { +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { try { console.log(JSON.stringify(event)); - console.log('AWS SDK VERSION: ' + AWS.VERSION); + console.log('AWS SDK VERSION: ' + (AWS as any).VERSION); let data = {}; @@ -27,7 +33,7 @@ exports.handler = async function(event, context) { if (event.ResourceProperties[event.RequestType]) { const { service, action, parameters = {} } = event.ResourceProperties[event.RequestType]; - const awsService = new AWS[service](); + const awsService = new (AWS as any)[service](); const response = await awsService[action](parameters).promise(); @@ -40,7 +46,7 @@ exports.handler = async function(event, context) { await respond('FAILED', e.message, context.logStreamName, {}); } - function respond(responseStatus, reason, physicalResourceId, data) { + function respond(responseStatus: string, reason: string, physicalResourceId: string, data: any) { const responseBody = JSON.stringify({ Status: responseStatus, Reason: reason, @@ -52,7 +58,7 @@ exports.handler = async function(event, context) { Data: data }); - console.log('Responding', JSON.stringify(responseBody)); + console.log('Responding', responseBody); const parsedUrl = require('url').parse(event.ResponseURL); const requestOptions = { diff --git a/packages/@aws-cdk/aws-cloudformation/package-lock.json b/packages/@aws-cdk/aws-cloudformation/package-lock.json index 5245173fd1bea..aeba741b5ec50 100644 --- a/packages/@aws-cdk/aws-cloudformation/package-lock.json +++ b/packages/@aws-cdk/aws-cloudformation/package-lock.json @@ -4,6 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/aws-lambda": { + "version": "8.10.23", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.23.tgz", + "integrity": "sha512-erfexxfuc1+T7b4OswooKwpIjpdgEOVz6ZrDDWSR+3v7Kjhs4EVowfUkF9KuLKhpcjz+VVHQ/pWIl7zSVbKbFQ==", + "dev": true + }, "@types/lodash": { "version": "4.14.120", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.120.tgz", diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index 10be660251b5e..87a45fe889f5a 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -61,6 +61,7 @@ "devDependencies": { "@aws-cdk/assert": "^0.26.0", "@aws-cdk/aws-events": "^0.26.0", + "@types/aws-lambda": "^8.10.23", "@types/lodash": "^4.14.118", "cdk-build-tools": "^0.26.0", "cdk-integ-tools": "^0.26.0", From 2fb5163c8f798dcc93b0676b2930a0e01ef77c7d Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 28 Mar 2019 17:36:46 +0100 Subject: [PATCH 10/23] Support path for physical resource id --- .../lib/aws-custom-resource-provider/index.ts | 23 +- .../lib/aws-custom-resource.ts | 99 +++--- .../aws-cloudformation/package-lock.json | 296 ++++++++++++++++++ .../@aws-cdk/aws-cloudformation/package.json | 7 +- .../integ.aws-custom-resource.expected.json | 62 ++-- .../test/integ.aws-custom-resource.ts | 8 +- .../test/test.aws-custom-resource-provider.ts | 152 +++++++++ .../test/test.aws-custom-resource.ts | 44 ++- 8 files changed, 574 insertions(+), 117 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts index 669156b9de45e..03e10bc2e4694 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts @@ -1,5 +1,6 @@ // tslint:disable:no-console import AWS = require('aws-sdk'); +import { AwsSdkCall } from '../aws-custom-resource'; /** * Flattens a nested object @@ -7,7 +8,7 @@ import AWS = require('aws-sdk'); * @param object the object to be flattened * @returns a flat object with path as keys */ -function flatten(object: object): object { +function flatten(object: object): { [key: string]: string } { return Object.assign( {}, ...function _flatten(child: any, path: string[] = []): any { @@ -26,18 +27,22 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent console.log(JSON.stringify(event)); console.log('AWS SDK VERSION: ' + (AWS as any).VERSION); - let data = {}; + let physicalResourceId = (event as any).PhysicalResourceId; + let data: { [key: string]: string } = {}; + const call: AwsSdkCall | undefined = event.ResourceProperties[event.RequestType]; - const physicalResourceId = event.ResourceProperties.PhysicalResourceId; + if (call) { + const awsService = new (AWS as any)[call.service](); - if (event.ResourceProperties[event.RequestType]) { - const { service, action, parameters = {} } = event.ResourceProperties[event.RequestType]; - - const awsService = new (AWS as any)[service](); - - const response = await awsService[action](parameters).promise(); + const response = await awsService[call.action](call.parameters).promise(); data = flatten(response); + + if (call.physicalResourceIdPath) { + physicalResourceId = data[call.physicalResourceIdPath]; + } else { + physicalResourceId = call.physicalResourceId!; + } } await respond('SUCCESS', 'OK', physicalResourceId, data); diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index 0a8912d066a8e..2363ea017dc3f 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -30,14 +30,27 @@ export interface AwsSdkCall { * The parameters for the service action */ readonly parameters?: any; -} -export interface AwsCustomResourceProps { /** - * The physical resource id of the custom resource. + * The path to the data in the API call response to use as the physical + * resource id. Either `physicalResourceId` or `physicalResourceIdPath` + * must be specified for onCreate or onUpdate calls. + * + * @default no path + */ + readonly physicalResourceIdPath?: string; + + /** + * The physical resource id of the custom resource for this call. Either + * `physicalResourceId` or `physicalResourceIdPath` must be specified for + * onCreate or onUpdate calls. + * + * @default no phyiscal resource id */ - readonly physicalResourceId: string; + readonly physicalResourceId?: string; +} +export interface AwsCustomResourceProps { /** * The AWS SDK call to make when the resource is created. * At least onCreate, onUpdate or onDelete must be specified. @@ -64,40 +77,12 @@ export interface AwsCustomResourceProps { * The IAM policy statements to allow the different calls. Use only if * resource restriction is needed. * - * @default Allow onCreate, onUpdate and onDelete calls on all resources ('*') + * @default extract the permissions for the calls */ readonly policyStatements?: iam.PolicyStatement[]; } export class AwsCustomResource extends cdk.Construct { - /** - * The physical resource id of the custom resource. - */ - public readonly physicalResourceId: string; - - /** - * The AWS SDK call made when the resource is created. - */ - public readonly onCreate?: AwsSdkCall; - - /** - * The AWS SDK call made when the resource is udpated. - */ - public readonly onUpdate?: AwsSdkCall; - - /** - * The AWS SDK call made when the resource is deleted. - */ - public readonly onDelete?: AwsSdkCall; - - /** - * The IAM policy statements used by the lambda provider. - */ - public readonly policyStatements: iam.PolicyStatement[]; - - /** - * The custom resource making the AWS SDK calls. - */ private readonly customResource: CustomResource; constructor(scope: cdk.Construct, id: string, props: AwsCustomResourceProps) { @@ -107,45 +92,47 @@ export class AwsCustomResource extends cdk.Construct { throw new Error('At least `onCreate`, `onUpdate` or `onDelete` must be specified.'); } - this.onCreate = props.onCreate || props.onUpdate; - this.onUpdate = props.onUpdate; - this.onDelete = props.onDelete; - this.physicalResourceId = props.physicalResourceId; + for (const call of [props.onCreate, props.onUpdate]) { + if (call && !call.physicalResourceId && !call.physicalResourceIdPath) { + throw new Error('Either `physicalResourceId` or `physicalResourceIdPath` must be specified for onCreate and onUpdate calls.'); + } + } - const fn = new lambda.SingletonFunction(this, 'Function', { + const provider = new lambda.SingletonFunction(this, 'Provider', { code: lambda.Code.asset(path.join(__dirname, 'aws-custom-resource-provider')), runtime: lambda.Runtime.NodeJS810, handler: 'index.handler', - uuid: '679f53fa-c002-430c-b0da-5b7982bd2287' + uuid: '679f53fa-c002-430c-b0da-5b7982bd2287', + lambdaPurpose: 'AWS' }); if (props.policyStatements) { - props.policyStatements.forEach(statement => { - fn.addToRolePolicy(statement); - }); - this.policyStatements = props.policyStatements; + for (const statement of props.policyStatements) { + provider.addToRolePolicy(statement); + } } else { // Derive statements from AWS SDK calls - this.policyStatements = []; + const statementActions: string[] = []; - [this.onCreate, this.onUpdate, this.onDelete].forEach(call => { - if (call) { - const statement = new iam.PolicyStatement() + for (const call of [props.onCreate, props.onUpdate, props.onDelete]) { + if (call && !statementActions.includes(`${call.service}-${call.action}`)) { // Avoid duplicate statements + provider.addToRolePolicy( + new iam.PolicyStatement() .addAction(awsSdkToIamAction(call.service, call.action)) - .addAllResources(); - fn.addToRolePolicy(statement); // TODO: remove duplicates? - this.policyStatements.push(statement); + .addAllResources() + ); + + statementActions.push(`${call.service}-${call.action}`); } - }); + } } this.customResource = new CustomResource(this, 'Resource', { resourceType: 'Custom::AWS', - lambdaProvider: fn, + lambdaProvider: provider, properties: { - physicalResourceId: this.physicalResourceId, - create: this.onCreate, - update: this.onUpdate, - delete: this.onDelete + create: props.onCreate || props.onUpdate, + update: props.onUpdate, + delete: props.onDelete } }); } diff --git a/packages/@aws-cdk/aws-cloudformation/package-lock.json b/packages/@aws-cdk/aws-cloudformation/package-lock.json index aeba741b5ec50..401e6c38112d3 100644 --- a/packages/@aws-cdk/aws-cloudformation/package-lock.json +++ b/packages/@aws-cdk/aws-cloudformation/package-lock.json @@ -4,6 +4,42 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", + "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/aws-lambda": { "version": "8.10.23", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.23.tgz", @@ -16,6 +52,39 @@ "integrity": "sha512-jQ21kQ120mo+IrDs1nFNVm/AsdFxIx2+vZ347DbogHJPd/JzKNMOqU6HCYin1W6v8l5R9XSO2/e9cxmn7HAnVw==", "dev": true }, + "@types/nock": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.1.tgz", + "integrity": "sha512-eOVHXS5RnWOjTVhu3deCM/ruy9E6JCgeix2g7wpFiekQh3AaEAK1cz43tZDukKmtSmQnwvSySq7ubijCA32I7Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.12.0.tgz", + "integrity": "sha512-Lg00egj78gM+4aE0Erw05cuDbvX9sLJbaaPwwRtdCdAMnIudqrQZ0oZX98Ek0yiSK/A2nubHgJfvII/rTT2Dwg==", + "dev": true + }, + "@types/sinon": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.10.tgz", + "integrity": "sha512-4w7SvsiUOtd4mUfund9QROPSJ5At/GQskDpqd87pJIRI6ULWSJqHI3GIZE337wQuN3aznroJGr94+o8fwvL37Q==", + "dev": true + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "aws-sdk": { "version": "2.409.0", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.409.0.tgz", @@ -32,6 +101,17 @@ "xml2js": "0.4.19" } }, + "aws-sdk-mock": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-4.3.1.tgz", + "integrity": "sha512-uOaf7/Tq9kSoRc2/EQfAn24AAwU6UwvR8xSFSg0vTRxK0xHHEZ5UB/KF6ibF2gj0I4977lM35237E5sbzhRxKA==", + "dev": true, + "requires": { + "aws-sdk": "^2.369.0", + "sinon": "^7.1.1", + "traverse": "^0.6.6" + } + }, "base64-js": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", @@ -47,11 +127,73 @@ "isarray": "^1.0.0" } }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "ieee754": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", @@ -67,17 +209,129 @@ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lolex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz", + "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "nise": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } + } + }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + } + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -88,6 +342,48 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "sinon": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.1.tgz", + "integrity": "sha512-eQKMaeWovtOtYe2xThEvaHmmxf870Di+bim10c3ZPrL5bZhLGtu8cz+rOBTFz0CwBV4Q/7dYwZiqZbGVLZ+vjQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.1", + "diff": "^3.5.0", + "lolex": "^3.1.0", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index 87a45fe889f5a..389ec2e5a4f60 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -63,11 +63,16 @@ "@aws-cdk/aws-events": "^0.26.0", "@types/aws-lambda": "^8.10.23", "@types/lodash": "^4.14.118", + "@types/nock": "^9.3.1", + "@types/sinon": "^7.0.10", + "aws-sdk-mock": "^4.3.1", "cdk-build-tools": "^0.26.0", "cdk-integ-tools": "^0.26.0", "cfn2ts": "^0.26.0", "lodash": "^4.17.11", - "pkglint": "^0.26.0" + "nock": "^10.0.6", + "pkglint": "^0.26.0", + "sinon": "^7.3.1" }, "dependencies": { "@aws-cdk/aws-codepipeline-api": "^0.26.0", diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json index 4527d5d0c4619..0161df5674b3e 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json @@ -8,13 +8,10 @@ "Properties": { "ServiceToken": { "Fn::GetAtt": [ - "SingletonLambda679f53fac002430cb0da5b7982bd2287AF41E197", + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn" ] }, - "PhysicalResourceId": { - "Ref": "TopicBFC7AF6E" - }, "Create": { "service": "SNS", "action": "publish", @@ -23,6 +20,9 @@ "TopicArn": { "Ref": "TopicBFC7AF6E" } + }, + "physicalResourceId": { + "Ref": "TopicBFC7AF6E" } }, "Update": { @@ -33,11 +33,14 @@ "TopicArn": { "Ref": "TopicBFC7AF6E" } + }, + "physicalResourceId": { + "Ref": "TopicBFC7AF6E" } } } }, - "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98": { + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -78,26 +81,16 @@ ] } }, - "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicy4215D22C": { + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { "Statement": [ - { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": "*" - }, { "Action": "sns:Publish", "Effect": "Allow", "Resource": "*" }, - { - "Action": "sns:ListTopics", - "Effect": "Allow", - "Resource": "*" - }, { "Action": "sns:ListTopics", "Effect": "Allow", @@ -106,20 +99,20 @@ ], "Version": "2012-10-17" }, - "PolicyName": "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicy4215D22C", + "PolicyName": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", "Roles": [ { - "Ref": "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98" + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" } ] } }, - "SingletonLambda679f53fac002430cb0da5b7982bd2287AF41E197": { + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { "S3Bucket": { - "Ref": "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3Bucket277B9EBE" + "Ref": "AWS679f53fac002430cb0da5b7982bd2287CodeS3BucketF55839B6" }, "S3Key": { "Fn::Join": [ @@ -132,7 +125,7 @@ "Fn::Split": [ "||", { - "Ref": "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3VersionKey7868E97C" + "Ref": "AWS679f53fac002430cb0da5b7982bd2287CodeS3VersionKey3C45B02F" } ] } @@ -145,7 +138,7 @@ "Fn::Split": [ "||", { - "Ref": "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3VersionKey7868E97C" + "Ref": "AWS679f53fac002430cb0da5b7982bd2287CodeS3VersionKey3C45B02F" } ] } @@ -158,15 +151,15 @@ "Handler": "index.handler", "Role": { "Fn::GetAtt": [ - "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98", + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, "Runtime": "nodejs8.10" }, "DependsOn": [ - "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicy4215D22C", - "SingletonLambda679f53fac002430cb0da5b7982bd2287ServiceRole905CAE98" + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" ] }, "ListTopicsCE1E0341": { @@ -174,32 +167,31 @@ "Properties": { "ServiceToken": { "Fn::GetAtt": [ - "SingletonLambda679f53fac002430cb0da5b7982bd2287AF41E197", + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn" ] }, - "PhysicalResourceId": { - "Ref": "TopicBFC7AF6E" - }, "Create": { "service": "SNS", - "action": "listTopics" + "action": "listTopics", + "physicalResourceIdPath": "Topics.0.TopicArn" }, "Update": { "service": "SNS", - "action": "listTopics" + "action": "listTopics", + "physicalResourceIdPath": "Topics.0.TopicArn" } } } }, "Parameters": { - "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3Bucket277B9EBE": { + "AWS679f53fac002430cb0da5b7982bd2287CodeS3BucketF55839B6": { "Type": "String", - "Description": "S3 bucket for asset \"aws-cdk-sdk-js/SingletonLambda679f53fac002430cb0da5b7982bd2287/Code\"" + "Description": "S3 bucket for asset \"aws-cdk-sdk-js/AWS679f53fac002430cb0da5b7982bd2287/Code\"" }, - "SingletonLambda679f53fac002430cb0da5b7982bd2287CodeS3VersionKey7868E97C": { + "AWS679f53fac002430cb0da5b7982bd2287CodeS3VersionKey3C45B02F": { "Type": "String", - "Description": "S3 key for asset version \"aws-cdk-sdk-js/SingletonLambda679f53fac002430cb0da5b7982bd2287/Code\"" + "Description": "S3 key for asset version \"aws-cdk-sdk-js/AWS679f53fac002430cb0da5b7982bd2287/Code\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts index ffb83a4a56b9a..790c95d49c2e0 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts @@ -10,22 +10,22 @@ const stack = new cdk.Stack(app, 'aws-cdk-sdk-js'); const topic = new sns.Topic(stack, 'Topic'); const snsPublish = new AwsCustomResource(stack, 'Publish', { - physicalResourceId: topic.topicArn, onUpdate: { service: 'SNS', action: 'publish', parameters: { Message: 'hello', TopicArn: topic.topicArn - } + }, + physicalResourceId: topic.topicArn, } }); const listTopics = new AwsCustomResource(stack, 'ListTopics', { - physicalResourceId: topic.topicArn, onUpdate: { service: 'SNS', - action: 'listTopics' + action: 'listTopics', + physicalResourceIdPath: 'Topics.0.TopicArn' } }); diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts new file mode 100644 index 0000000000000..b4233bd4685c7 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts @@ -0,0 +1,152 @@ +import SDK = require('aws-sdk'); +import AWS = require('aws-sdk-mock'); +import nock = require('nock'); +import { Test } from 'nodeunit'; +import sinon = require('sinon'); +import { AwsSdkCall } from '../lib'; +import { handler } from '../lib/aws-custom-resource-provider'; + +const eventCommon = { + ServiceToken: 'token', + ResponseURL: 'https://localhost', + StackId: 'stackId', + RequestId: 'requestId', + LogicalResourceId: 'logicalResourceId', + ResourceType: 'Custom::AWS', +}; + +function createRequest(bodyPredicate: (body: AWSLambda.CloudFormationCustomResourceResponse) => boolean) { + return nock('https://localhost') + .put('/', bodyPredicate) + .reply(200); +} + +export = { + 'tearDown'(callback: any) { + AWS.restore(); + nock.cleanAll(); + callback(); + }, + + async 'create event with physical resource id path'(test: Test) { + const listObjectsFake = sinon.fake.resolves({ + Contents: [ + { + Key: 'first-key', + ETag: 'first-key-etag' + }, + { + Key: 'second-key', + ETag: 'second-key-etag', + } + ] + } as SDK.S3.ListObjectsOutput); + + AWS.mock('S3', 'listObjects', listObjectsFake); + + const event: AWSLambda.CloudFormationCustomResourceCreateEvent = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + Create: { + service: 'S3', + action: 'listObjects', + parameters: { + Bucket: 'my-bucket' + }, + physicalResourceIdPath: 'Contents.1.ETag' + } as AwsSdkCall + } + }; + + const request = createRequest(body => + body.Status === 'SUCCESS' && + body.PhysicalResourceId === 'second-key-etag' && + body.Data!['Contents.0.Key'] === 'first-key' + ); + + await handler(event, {} as AWSLambda.Context); + + sinon.assert.calledWith(listObjectsFake, { + Bucket: 'my-bucket' + }); + + test.equal(request.isDone(), true); + + test.done(); + }, + + async 'update event with physical resource id'(test: Test) { + const publish = sinon.fake.resolves({}); + + AWS.mock('SNS', 'publish', publish); + + const event: AWSLambda.CloudFormationCustomResourceUpdateEvent = { + ...eventCommon, + RequestType: 'Update', + PhysicalResourceId: 'physicalResourceId', + OldResourceProperties: {}, + ResourceProperties: { + ServiceToken: 'token', + Update: { + service: 'SNS', + action: 'publish', + parameters: { + Message: 'hello', + TopicArn: 'topicarn' + }, + physicalResourceId: 'topicarn' + } as AwsSdkCall + } + }; + + const request = createRequest(body => + body.Status === 'SUCCESS' && + body.PhysicalResourceId === 'topicarn' + ); + + await handler(event, {} as AWSLambda.Context); + + test.equal(request.isDone(), true); + + test.done(); + }, + + async 'delete event'(test: Test) { + const listObjectsFake = sinon.fake.resolves({}); + + AWS.mock('S3', 'listObjects', listObjectsFake); + + const event: AWSLambda.CloudFormationCustomResourceDeleteEvent = { + ...eventCommon, + RequestType: 'Delete', + PhysicalResourceId: 'physicalResourceId', + ResourceProperties: { + ServiceToken: 'token', + Create: { + service: 'S3', + action: 'listObjects', + parameters: { + Bucket: 'my-bucket' + }, + physicalResourceIdPath: 'Contents.1.ETag' + } as AwsSdkCall + } + }; + + const request = createRequest(body => + body.Status === 'SUCCESS' && + body.PhysicalResourceId === 'physicalResourceId' && + Object.keys(body.Data!).length === 0 + ); + + await handler(event, {} as AWSLambda.Context); + + sinon.assert.notCalled(listObjectsFake); + + test.equal(request.isDone(), true); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource.ts index 3ccb1270300af..43dd116a5f46e 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource.ts @@ -13,14 +13,14 @@ export = { // WHEN new AwsCustomResource(stack, 'AwsSdk', { - physicalResourceId: 'id1234', onCreate: { service: 'CloudWatchLogs', action: 'putRetentionPolicy', parameters: { logGroupName: '/aws/lambda/loggroup', retentionInDays: 90 - } + }, + physicalResourceId: 'loggroup' }, onDelete: { service: 'CloudWatchLogs', @@ -33,14 +33,14 @@ export = { // THEN expect(stack).to(haveResource('Custom::AWS', { - "PhysicalResourceId": "id1234", "Create": { "service": "CloudWatchLogs", "action": "putRetentionPolicy", "parameters": { "logGroupName": "/aws/lambda/loggroup", "retentionInDays": 90 - } + }, + "physicalResourceId": "loggroup" }, "Delete": { "service": "CloudWatchLogs", @@ -78,7 +78,6 @@ export = { // WHEN new AwsCustomResource(stack, 'AwsSdk', { - physicalResourceId: 'id1234', onUpdate: { service: 's3', action: 'putObject', @@ -86,13 +85,13 @@ export = { Bucket: 'my-bucket', Key: 'my-key', Body: 'my-body' - } + }, + physicalResourceIdPath: 'ETag' }, }); // THEN expect(stack).to(haveResource('Custom::AWS', { - "PhysicalResourceId": "id1234", "Create": { "service": "s3", "action": "putObject", @@ -100,7 +99,8 @@ export = { "Bucket": "my-bucket", "Key": "my-key", "Body": "my-body" - } + }, + "physicalResourceIdPath": "ETag" }, "Update": { "service": "s3", @@ -109,7 +109,8 @@ export = { "Bucket": "my-bucket", "Key": "my-key", "Body": "my-body" - } + }, + "physicalResourceIdPath": "ETag" }, })); @@ -122,7 +123,6 @@ export = { // WHEN new AwsCustomResource(stack, 'AwsSdk', { - physicalResourceId: 'id1234', onUpdate: { service: 'S3', action: 'putObject', @@ -130,7 +130,8 @@ export = { Bucket: 'my-bucket', Key: 'my-key', Body: 'my-body' - } + }, + physicalResourceIdPath: 'ETag' }, policyStatements: [ new iam.PolicyStatement() @@ -160,9 +161,28 @@ export = { const stack = new cdk.Stack(); test.throws(() => { - new AwsCustomResource(stack, 'AwsSdk', { physicalResourceId: 'id1234' }); + new AwsCustomResource(stack, 'AwsSdk', {}); }, /`onCreate`.+`onUpdate`.+`onDelete`/); + test.done(); + }, + + 'fails when no physical resource method is specified'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new AwsCustomResource(stack, 'AwsSdk', { + onUpdate: { + service: 'CloudWatchLogs', + action: 'putRetentionPolicy', + parameters: { + logGroupName: '/aws/lambda/loggroup', + retentionInDays: 90 + } + } + }); + }, /`physicalResourceId`.+`physicalResourceIdPath`/); + test.done(); } }; From 04c8df508c665d527d845cf71d744ab74302bee9 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 28 Mar 2019 21:29:50 +0100 Subject: [PATCH 11/23] Typo --- .../@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index 2363ea017dc3f..d3aa9fb59a924 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -45,7 +45,7 @@ export interface AwsSdkCall { * `physicalResourceId` or `physicalResourceIdPath` must be specified for * onCreate or onUpdate calls. * - * @default no phyiscal resource id + * @default no physical resource id */ readonly physicalResourceId?: string; } @@ -77,7 +77,7 @@ export interface AwsCustomResourceProps { * The IAM policy statements to allow the different calls. Use only if * resource restriction is needed. * - * @default extract the permissions for the calls + * @default extract the permissions from the calls */ readonly policyStatements?: iam.PolicyStatement[]; } From 82d1ea10e02be3b6ed6e511521d9192fd69d1eed Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 28 Mar 2019 23:22:41 +0100 Subject: [PATCH 12/23] Fix bad pkglint version after merge --- packages/@aws-cdk/aws-cloudformation/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index e5a36d76ad37a..86072c596f061 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -75,7 +75,7 @@ "cfn2ts": "^0.27.0", "lodash": "^4.17.11", "nock": "^10.0.6", - "pkglint": "^0.26.0", + "pkglint": "^0.27.0", "sinon": "^7.3.1" }, "dependencies": { From a26be818272dbc2e8ea22370c6cef3e0542b1133 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 2 Apr 2019 10:12:45 +0200 Subject: [PATCH 13/23] Add option to catch API errors --- .../lib/aws-custom-resource-provider/index.ts | 11 ++++-- .../lib/aws-custom-resource.ts | 9 +++++ .../test/test.aws-custom-resource-provider.ts | 37 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts index 03e10bc2e4694..d1daec8df34c3 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts @@ -34,9 +34,14 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent if (call) { const awsService = new (AWS as any)[call.service](); - const response = await awsService[call.action](call.parameters).promise(); - - data = flatten(response); + try { + const response = await awsService[call.action](call.parameters).promise(); + data = flatten(response); + } catch (e) { + if (!call.catchErrorPattern || !new RegExp(call.catchErrorPattern).test(e.code)) { + throw e; + } + } if (call.physicalResourceIdPath) { physicalResourceId = data[call.physicalResourceIdPath]; diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index d3aa9fb59a924..b97bb887bf23a 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -48,6 +48,15 @@ export interface AwsSdkCall { * @default no physical resource id */ readonly physicalResourceId?: string; + + /** + * The regex pattern to use to catch API errors. The `code` property of the + * `Error` object will be tested against this pattern. If there is a match an + * error will not be thrown. + * + * @default do not catch errors + */ + readonly catchErrorPattern?: string; } export interface AwsCustomResourceProps { diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts index b4233bd4685c7..7a8535517935a 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts @@ -147,6 +147,43 @@ export = { test.equal(request.isDone(), true); + test.done(); + }, + + async 'catch errors'(test: Test) { + const error: NodeJS.ErrnoException = new Error(); + error.code = 'NoSuchBucket'; + const listObjectsFake = sinon.fake.rejects(error); + + AWS.mock('S3', 'listObjects', listObjectsFake); + + const event: AWSLambda.CloudFormationCustomResourceCreateEvent = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + Create: { + service: 'S3', + action: 'listObjects', + parameters: { + Bucket: 'my-bucket' + }, + physicalResourceId: 'physicalResourceId', + catchErrorPattern: 'NoSuchBucket' + } as AwsSdkCall + } + }; + + const request = createRequest(body => + body.Status === 'SUCCESS' && + body.PhysicalResourceId === 'physicalResourceId' && + Object.keys(body.Data!).length === 0 + ); + + await handler(event, {} as AWSLambda.Context); + + test.equal(request.isDone(), true); + test.done(); } }; From b5fc424d470c517f7be9f2d8d9df0b58e1f2005f Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 2 Apr 2019 10:20:00 +0200 Subject: [PATCH 14/23] Restore package-lock.json in aws-codepipeline-actions --- .../package-lock.json | 396 ------------------ 1 file changed, 396 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package-lock.json b/packages/@aws-cdk/aws-codepipeline-actions/package-lock.json index 8e40b3e815bb3..7c59ec4be6731 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package-lock.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package-lock.json @@ -4,413 +4,17 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "@sinonjs/samsam": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", - "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true - }, - "@types/aws-lambda": { - "version": "8.10.23", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.23.tgz", - "integrity": "sha512-erfexxfuc1+T7b4OswooKwpIjpdgEOVz6ZrDDWSR+3v7Kjhs4EVowfUkF9KuLKhpcjz+VVHQ/pWIl7zSVbKbFQ==", - "dev": true - }, "@types/lodash": { "version": "4.14.120", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.120.tgz", "integrity": "sha512-jQ21kQ120mo+IrDs1nFNVm/AsdFxIx2+vZ347DbogHJPd/JzKNMOqU6HCYin1W6v8l5R9XSO2/e9cxmn7HAnVw==", "dev": true }, - "@types/nock": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.1.tgz", - "integrity": "sha512-eOVHXS5RnWOjTVhu3deCM/ruy9E6JCgeix2g7wpFiekQh3AaEAK1cz43tZDukKmtSmQnwvSySq7ubijCA32I7Q==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.12.0.tgz", - "integrity": "sha512-Lg00egj78gM+4aE0Erw05cuDbvX9sLJbaaPwwRtdCdAMnIudqrQZ0oZX98Ek0yiSK/A2nubHgJfvII/rTT2Dwg==", - "dev": true - }, - "@types/sinon": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.10.tgz", - "integrity": "sha512-4w7SvsiUOtd4mUfund9QROPSJ5At/GQskDpqd87pJIRI6ULWSJqHI3GIZE337wQuN3aznroJGr94+o8fwvL37Q==", - "dev": true - }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "aws-sdk": { - "version": "2.409.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.409.0.tgz", - "integrity": "sha512-QV6j9zBQq/Kz8BqVOrJ03ABjMKtErXdUT1YdYEljoLQZimpzt0ZdQwJAsoZIsxxriOJgrqeZsQUklv9AFQaldQ==", - "requires": { - "buffer": "4.9.1", - "events": "1.1.1", - "ieee754": "1.1.8", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - } - }, - "aws-sdk-mock": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-4.3.1.tgz", - "integrity": "sha512-uOaf7/Tq9kSoRc2/EQfAn24AAwU6UwvR8xSFSg0vTRxK0xHHEZ5UB/KF6ibF2gj0I4977lM35237E5sbzhRxKA==", - "dev": true, - "requires": { - "aws-sdk": "^2.369.0", - "sinon": "^7.1.1", - "traverse": "^0.6.6" - } - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ieee754": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", - "dev": true - }, "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true - }, - "lolex": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz", - "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==", - "dev": true - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "nise": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", - "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", - "dev": true, - "requires": { - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^2.3.2", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", - "dev": true - } - } - }, - "nock": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", - "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", - "dev": true, - "requires": { - "chai": "^4.1.2", - "debug": "^4.1.0", - "deep-equal": "^1.0.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.5", - "mkdirp": "^0.5.0", - "propagate": "^1.0.0", - "qs": "^6.5.1", - "semver": "^5.5.0" - } - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "propagate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", - "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", - "dev": true - }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - }, - "sinon": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.1.tgz", - "integrity": "sha512-eQKMaeWovtOtYe2xThEvaHmmxf870Di+bim10c3ZPrL5bZhLGtu8cz+rOBTFz0CwBV4Q/7dYwZiqZbGVLZ+vjQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.1", - "diff": "^3.5.0", - "lolex": "^3.1.0", - "nise": "^1.4.10", - "supports-color": "^5.5.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", - "dev": true - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" } } } From 1b7f3920d3b84eb0abdeaf034fa323ce5d8adcdd Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 13 May 2019 15:53:46 +0200 Subject: [PATCH 15/23] CustomResourceProvider --- .../@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index b97bb887bf23a..550a1c9aa2e1c 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -3,7 +3,7 @@ import lambda = require('@aws-cdk/aws-lambda'); import cdk = require('@aws-cdk/cdk'); import metadata = require('aws-sdk/apis/metadata.json'); import path = require('path'); -import { CustomResource } from './custom-resource'; +import { CustomResource, CustomResourceProvider } from './custom-resource'; /** * AWS SDK service metadata. @@ -137,7 +137,7 @@ export class AwsCustomResource extends cdk.Construct { this.customResource = new CustomResource(this, 'Resource', { resourceType: 'Custom::AWS', - lambdaProvider: provider, + provider: CustomResourceProvider.lambda(provider), properties: { create: props.onCreate || props.onUpdate, update: props.onUpdate, From 598f60546b764855ddf4e6cc5dcd8b0c92bc8faa Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 13 May 2019 15:54:03 +0200 Subject: [PATCH 16/23] exclude construct-ctor-props-optional --- packages/@aws-cdk/aws-cloudformation/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index a1ed37095f437..0f94f694046e9 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -94,7 +94,8 @@ "awslint": { "exclude": [ "construct-ctor:@aws-cdk/aws-cloudformation.PipelineCloudFormationAction.", - "construct-ctor:@aws-cdk/aws-cloudformation.PipelineCloudFormationDeployAction." + "construct-ctor:@aws-cdk/aws-cloudformation.PipelineCloudFormationDeployAction.", + "construct-ctor-props-optional:@aws-cdk/aws-cloudformation.AwsCustomResource" ] } } From 290d77c315533b981aaf185c7d314d2f6aa2a447 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 13 May 2019 15:56:26 +0200 Subject: [PATCH 17/23] remove duplicate statements check --- .../aws-cloudformation/lib/aws-custom-resource.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index 550a1c9aa2e1c..6157bbd47c1da 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -120,17 +120,13 @@ export class AwsCustomResource extends cdk.Construct { provider.addToRolePolicy(statement); } } else { // Derive statements from AWS SDK calls - const statementActions: string[] = []; - for (const call of [props.onCreate, props.onUpdate, props.onDelete]) { - if (call && !statementActions.includes(`${call.service}-${call.action}`)) { // Avoid duplicate statements + if (call) { provider.addToRolePolicy( new iam.PolicyStatement() - .addAction(awsSdkToIamAction(call.service, call.action)) - .addAllResources() + .addAction(awsSdkToIamAction(call.service, call.action)) + .addAllResources() ); - - statementActions.push(`${call.service}-${call.action}`); } } } From 89816001447a630d4f4815a0e48de00583592831 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 13 May 2019 18:14:13 +0200 Subject: [PATCH 18/23] add option to lock api version --- .../lib/aws-custom-resource-provider/index.ts | 2 +- .../aws-cloudformation/lib/aws-custom-resource.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts index d1daec8df34c3..b61cd94bcb38e 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts @@ -32,7 +32,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent const call: AwsSdkCall | undefined = event.ResourceProperties[event.RequestType]; if (call) { - const awsService = new (AWS as any)[call.service](); + const awsService = new (AWS as any)[call.service](call.apiVersion && { apiVersion: call.apiVersion }); try { const response = await awsService[call.action](call.parameters).promise(); diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index 6157bbd47c1da..4f4b28ff424dd 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -57,6 +57,14 @@ export interface AwsSdkCall { * @default do not catch errors */ readonly catchErrorPattern?: string; + + /** + * API version to use for the service + * + * @see https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/locking-api-versions.html + * @default use latest available API version + */ + readonly apiVersion?: string; } export interface AwsCustomResourceProps { From c423f357aadc9f45b332452aa0cb591c6e646038 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 13 May 2019 20:34:43 +0200 Subject: [PATCH 19/23] JSDoc --- .../@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts index 4f4b28ff424dd..7a0b7b657a8a8 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource.ts @@ -18,16 +18,22 @@ const awsSdkMetadata: AwsSdkMetadata = metadata; export interface AwsSdkCall { /** * The service to call + * + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html */ readonly service: string; /** * The service action to call + * + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html */ readonly action: string; /** * The parameters for the service action + * + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html */ readonly parameters?: any; From d7b66cc7a286ec452a007bc5fcef44e1df4e85e3 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 14 May 2019 10:57:14 +0200 Subject: [PATCH 20/23] fix booleans in parameters --- .../lib/aws-custom-resource-provider/index.ts | 13 ++++++- .../test/test.aws-custom-resource-provider.ts | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts index b61cd94bcb38e..5dcf0d9c2e14f 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts @@ -22,6 +22,17 @@ function flatten(object: object): { [key: string]: string } { ); } +/** + * Converts true/false strings to booleans in an object + */ +function fixBooleans(object: object) { + return JSON.parse(JSON.stringify(object), (_k, v) => v === 'true' + ? true + : v === 'false' + ? false + : v); +} + export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { try { console.log(JSON.stringify(event)); @@ -35,7 +46,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent const awsService = new (AWS as any)[call.service](call.apiVersion && { apiVersion: call.apiVersion }); try { - const response = await awsService[call.action](call.parameters).promise(); + const response = await awsService[call.action](call.parameters && fixBooleans(call.parameters)).promise(); data = flatten(response); } catch (e) { if (!call.catchErrorPattern || !new RegExp(call.catchErrorPattern).test(e.code)) { diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts index 7a8535517935a..a28f7035acad3 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.aws-custom-resource-provider.ts @@ -184,6 +184,44 @@ export = { test.equal(request.isDone(), true); + test.done(); + }, + + async 'fixes booleans'(test: Test) { + const getParameterFake = sinon.fake.resolves({}); + + AWS.mock('SSM', 'getParameter', getParameterFake); + + const event: AWSLambda.CloudFormationCustomResourceCreateEvent = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + Create: { + service: 'SSM', + action: 'getParameter', + parameters: { + Name: 'my-parameter', + WithDecryption: 'true' + }, + physicalResourceId: 'my-parameter' + } as AwsSdkCall + } + }; + + const request = createRequest(body => + body.Status === 'SUCCESS' + ); + + await handler(event, {} as AWSLambda.Context); + + sinon.assert.calledWith(getParameterFake, { + Name: 'my-parameter', + WithDecryption: true // boolean + }); + + test.equal(request.isDone(), true); + test.done(); } }; From e8813f546bbb3f544723e85b4910b7bf31b7e64d Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 14 May 2019 10:57:56 +0200 Subject: [PATCH 21/23] update integration test --- .../integ.aws-custom-resource.expected.json | 42 +++++++++++++++++++ .../test/integ.aws-custom-resource.ts | 13 ++++++ 2 files changed, 55 insertions(+) diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json index 0161df5674b3e..41dd961454a70 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json @@ -95,6 +95,11 @@ "Action": "sns:ListTopics", "Effect": "Allow", "Resource": "*" + }, + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": "*" } ], "Version": "2012-10-17" @@ -182,6 +187,35 @@ "physicalResourceIdPath": "Topics.0.TopicArn" } } + }, + "GetParameter42B0A00E": { + "Type": "Custom::AWS", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "service": "SSM", + "action": "getParameter", + "parameters": { + "Name": "my-parameter", + "WithDecryption": true + }, + "physicalResourceIdPath": "Parameter.ARN" + }, + "Update": { + "service": "SSM", + "action": "getParameter", + "parameters": { + "Name": "my-parameter", + "WithDecryption": true + }, + "physicalResourceIdPath": "Parameter.ARN" + } + } } }, "Parameters": { @@ -210,6 +244,14 @@ "Topics.0.TopicArn" ] } + }, + "ParameterValue": { + "Value": { + "Fn::GetAtt": [ + "GetParameter42B0A00E", + "Parameter.Value" + ] + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts index 790c95d49c2e0..92d707ebc1ea6 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.ts @@ -29,7 +29,20 @@ const listTopics = new AwsCustomResource(stack, 'ListTopics', { } }); +const getParameter = new AwsCustomResource(stack, 'GetParameter', { + onUpdate: { + service: 'SSM', + action: 'getParameter', + parameters: { + Name: 'my-parameter', + WithDecryption: true + }, + physicalResourceIdPath: 'Parameter.ARN' + } +}); + new cdk.CfnOutput(stack, 'MessageId', { value: snsPublish.getData('MessageId') }); new cdk.CfnOutput(stack, 'TopicArn', { value: listTopics.getData('Topics.0.TopicArn') }); +new cdk.CfnOutput(stack, 'ParameterValue', { value: getParameter.getData('Parameter.Value') }); app.run(); From 1b204d8290ba59e756d51aea95c4820a55f09c20 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 14 May 2019 12:00:37 +0200 Subject: [PATCH 22/23] update README --- .../@aws-cdk/aws-cloudformation/README.md | 127 +++++++++++++++--- 1 file changed, 106 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudformation/README.md b/packages/@aws-cdk/aws-cloudformation/README.md index 20a82664f5aba..6f6367e28b110 100644 --- a/packages/@aws-cdk/aws-cloudformation/README.md +++ b/packages/@aws-cdk/aws-cloudformation/README.md @@ -30,30 +30,30 @@ Sample of a Custom Resource that copies files into an S3 bucket during deploymen ```ts interface CopyOperationProps { - sourceBucket: IBucket; - targetBucket: IBucket; + sourceBucket: IBucket; + targetBucket: IBucket; } class CopyOperation extends Construct { - constructor(parent: Construct, name: string, props: DemoResourceProps) { - super(parent, name); - - const lambdaProvider = new SingletonLambda(this, 'Provider', { - uuid: 'f7d4f730-4ee1-11e8-9c2d-fa7ae01bbebc', - code: new LambdaInlineCode(resources['copy.py']), - handler: 'index.handler', - timeout: 60, - runtime: LambdaRuntime.Python3, - }); - - new CustomResource(this, 'Resource', { - lambdaProvider, - properties: { - sourceBucketArn: props.sourceBucket.bucketArn, - targetBucketArn: props.targetBucket.bucketArn, - } - }); - } + constructor(parent: Construct, name: string, props: DemoResourceProps) { + super(parent, name); + + const lambdaProvider = new SingletonLambda(this, 'Provider', { + uuid: 'f7d4f730-4ee1-11e8-9c2d-fa7ae01bbebc', + code: new LambdaInlineCode(resources['copy.py']), + handler: 'index.handler', + timeout: 60, + runtime: LambdaRuntime.Python3, + }); + + new CustomResource(this, 'Resource', { + provider: CustomResourceProvider.lambda(provider), + properties: { + sourceBucketArn: props.sourceBucket.bucketArn, + targetBucketArn: props.targetBucket.bucketArn, + } + }); + } } ``` @@ -67,3 +67,88 @@ See the following section of the docs on details to write Custom Resources: * [Introduction](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) * [Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref.html) * [Code Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html) + +#### AWS Custom Resource +Sometimes a single API call can fill the gap in the CloudFormation coverage. In +this case you can use the `AwsCustomResource` construct. This construct creates +a custom resource that can be customized to make specific API calls for the +`CREATE`, `UPDATE` and `DELETE` events. Additionally, data returned by the API +call can be extracted and used in other constructs/resources (creating a real +CloudFormation dependency using `Fn::GetAtt` under the hood). + +The physical id of the custom resource can be specified or derived from the data +return by the API call. + +The `AwsCustomResource` uses the AWS SDK for JavaScript. Services, actions and +parameters can be found in the [API documentation](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html). + +Path to data must be specified using a dot notation, e.g. to get the string value +of the `Title` attribute for the first item returned by `dynamodb.query` it should +be `Items.0.Title.S`. + +##### Examples +Verify a domain with SES: + +```ts +const verifyDomainIdentity = new AwsCustomResource(this, 'VerifyDomainIdentity', { + onCreate: { + service: 'SES', + action: 'verifyDomainIdentity', + parameters: { + Domain: 'example.com' + }, + physicalResourceIdPath: 'VerificationToken' // Use the token returned by the call as physical id + } +}); + +new route53.TxtRecord(zone, 'SESVerificationRecord', { + recordName: `_amazonses.example.com`, + recordValue: verifyDomainIdentity.getData('VerificationToken') +}); +``` + +Get the latest version of a secure SSM parameter: + +```ts +const getParameter = new AwsCustomResource(this, 'GetParameter', { + onUpdate: { // will also be called for a CREATE event + service: 'SSM', + action: 'getParameter', + parameters: { + Name: 'my-parameter', + WithDecryption: true + }, + physicalResourceId: Date.now().toString() // Update physical id to always fetch the latest version + } +}); + +// Use the value in another construct with +getParameter.getData('Parameter.Value') +``` + +IAM policy statements required to make the API calls are derived from the calls +and allow by default the actions to be made on all resources (`*`). You can +restrict the permissions by specifying your own list of statements with the +`policyStatements` prop. + +Chained API calls can be achieved by creating dependencies: +```ts +const awsCustom1 = new AwsCustomResource(this, 'API1', { + onCreate: { + service: '...', + action: '...', + physicalResourceId: '...' + } +}); + +const awsCustom2 = new AwsCustomResource(this, 'API2', { + onCreate: { + service: '...', + action: '...' + parameters: { + text: awsCustom1.getData('Items.0.text') + }, + physicalResourceId: '...' + } +}) +``` From e02480f7885a3e713330b1ba17c88e9a64cb23de Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 27 May 2019 11:50:04 +0200 Subject: [PATCH 23/23] update integ test --- .../test/integ.aws-custom-resource.expected.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json index 41dd961454a70..2ed3d93227ae6 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.aws-custom-resource.expected.json @@ -226,6 +226,10 @@ "AWS679f53fac002430cb0da5b7982bd2287CodeS3VersionKey3C45B02F": { "Type": "String", "Description": "S3 key for asset version \"aws-cdk-sdk-js/AWS679f53fac002430cb0da5b7982bd2287/Code\"" + }, + "AWS679f53fac002430cb0da5b7982bd2287CodeArtifactHash49FACC2E": { + "Type": "String", + "Description": "Artifact hash for asset \"aws-cdk-sdk-js/AWS679f53fac002430cb0da5b7982bd2287/Code\"" } }, "Outputs": {