From a111cdd97928280b206c3dcfc522e642106e3a70 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser <jogold@users.noreply.github.com> Date: Wed, 18 Dec 2019 12:43:07 +0100 Subject: [PATCH] feat(custom-resources): use latest SDK in AwsCustomResource (#5442) Install the latest v2 of AWS SDK JS when a new container is initialized for the Lambda function. Subsequent executions reusing this container will skip installation. Increase default timeout to 60 seconds. Closes #2689 Closes #5063 --- .../aws-custom-resource.ts | 4 +- .../lib/aws-custom-resource/runtime/index.ts | 35 +++++++++++++-- .../@aws-cdk/custom-resources/package.json | 4 +- .../aws-custom-resource-provider.test.ts | 43 +++++++++++++++++++ .../aws-custom-resource.test.ts | 2 +- .../integ.aws-custom-resource.expected.json | 20 ++++----- 6 files changed, 91 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index 895c6f7a67dad..56b5f63a581a9 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -149,7 +149,7 @@ export interface AwsCustomResourceProps { /** * The timeout for the Lambda function implementing this custom resource. * - * @default Duration.seconds(30) + * @default Duration.seconds(60) */ readonly timeout?: cdk.Duration } @@ -178,7 +178,7 @@ export class AwsCustomResource extends cdk.Construct implements iam.IGrantable { handler: 'index.handler', uuid: '679f53fa-c002-430c-b0da-5b7982bd2287', lambdaPurpose: 'AWS', - timeout: props.timeout || cdk.Duration.seconds(30), + timeout: props.timeout || cdk.Duration.seconds(60), role: props.role, }); this.grantPrincipal = provider.grantPrincipal; diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts index 86f1c123784fc..47e7e03319485 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts @@ -1,6 +1,5 @@ // tslint:disable:no-console -// eslint-disable-next-line import/no-extraneous-dependencies -import * as AWS from 'aws-sdk'; +import { execSync } from 'child_process'; import { AwsSdkCall } from '../aws-custom-resource'; /** @@ -52,10 +51,40 @@ function filterKeys(object: object, pred: (key: string) => boolean) { ); } +let latestSdkInstalled = false; + +/** + * Installs latest AWS SDK v2 + */ +function installLatestSdk(): void { + console.log('Installing latest AWS SDK v2'); + // Both HOME and --prefix are needed here because /tmp is the only writable location + execSync('HOME=/tmp npm install aws-sdk@2 --production --no-package-lock --no-save --prefix /tmp'); + latestSdkInstalled = true; +} + +/* eslint-disable @typescript-eslint/no-require-imports, import/no-extraneous-dependencies */ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { try { + let AWS: any; + if (!latestSdkInstalled) { + try { + installLatestSdk(); + AWS = require('/tmp/node_modules/aws-sdk'); + } catch (e) { + console.log(`Failed to install latest AWS SDK v2: ${e}`); + AWS = require('aws-sdk'); // Fallback to pre-installed version + } + } else { + AWS = require('/tmp/node_modules/aws-sdk'); + } + + if (process.env.USE_NORMAL_SDK) { // For tests only + AWS = require('aws-sdk'); + } + console.log(JSON.stringify(event)); - console.log('AWS SDK VERSION: ' + (AWS as any).VERSION); + console.log('AWS SDK VERSION: ' + AWS.VERSION); let physicalResourceId = (event as any).PhysicalResourceId; let flatData: { [key: string]: string } = {}; diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index c19968119ee56..c6bc761ad6fd4 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -71,12 +71,14 @@ "@aws-cdk/aws-s3": "1.19.0", "@aws-cdk/aws-ssm": "1.19.0", "@types/aws-lambda": "^8.10.37", + "@types/fs-extra": "^8.0.1", "@types/sinon": "^7.5.0", "aws-sdk": "^2.590.0", "aws-sdk-mock": "^4.5.0", "cdk-build-tools": "1.19.0", "cdk-integ-tools": "1.19.0", "cfn2ts": "1.19.0", + "fs-extra": "^8.1.0", "nock": "^11.7.0", "pkglint": "1.19.0", "sinon": "^7.5.0" @@ -124,4 +126,4 @@ "props-default-doc:@aws-cdk/custom-resources.AwsSdkCall.parameters" ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts index 16f9a09b0a2c2..8a5b4da815271 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts @@ -1,5 +1,6 @@ import * as SDK from 'aws-sdk'; import * as AWS from 'aws-sdk-mock'; +import * as fs from 'fs-extra'; import * as nock from 'nock'; import * as sinon from 'sinon'; import { AwsSdkCall } from '../../lib'; @@ -24,9 +25,14 @@ function createRequest(bodyPredicate: (body: AWSLambda.CloudFormationCustomResou .reply(200); } +beforeEach(() => { + process.env.USE_NORMAL_SDK = 'true'; +}); + afterEach(() => { AWS.restore(); nock.cleanAll(); + delete process.env.USE_NORMAL_SDK; }); test('create event with physical resource id path', async () => { @@ -338,3 +344,40 @@ test('flatten correctly flattens a nested object', () => { 'd.1.k.l': false }); }); + +test('installs the latest SDK', async () => { + const tmpPath = '/tmp/node_modules/aws-sdk'; + + fs.remove(tmpPath); + + const publishFake = sinon.fake.resolves({}); + + AWS.mock('SNS', 'publish', publishFake); + + const event: AWSLambda.CloudFormationCustomResourceCreateEvent = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + Create: { + service: 'SNS', + action: 'publish', + parameters: { + Message: 'message', + TopicArn: 'topic' + }, + physicalResourceId: 'id', + } as AwsSdkCall + } + }; + + const request = createRequest(body => + body.Status === 'SUCCESS' + ); + + await handler(event, {} as AWSLambda.Context); + + expect(request.isDone()).toBeTruthy(); + + expect(() => require.resolve(tmpPath)).not.toThrow(); +}); diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts index 2eb5bfc85e0dc..87ded6b98cce3 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts @@ -220,7 +220,7 @@ test('timeout defaults to 30 seconds', () => { // THEN expect(stack).toHaveResource('AWS::Lambda::Function', { - Timeout: 30 + Timeout: 60 }); }); diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json index e38c1f0fcb40d..f7bc469e9b2b4 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json @@ -109,7 +109,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8S3BucketDA18872A" + "Ref": "AssetParameters138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5eS3BucketC954C005" }, "S3Key": { "Fn::Join": [ @@ -122,7 +122,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8S3VersionKey8DEA118B" + "Ref": "AssetParameters138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5eS3VersionKey2D066AE2" } ] } @@ -135,7 +135,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8S3VersionKey8DEA118B" + "Ref": "AssetParameters138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5eS3VersionKey2D066AE2" } ] } @@ -153,7 +153,7 @@ ] }, "Runtime": "nodejs12.x", - "Timeout": 30 + "Timeout": 60 }, "DependsOn": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E", @@ -230,17 +230,17 @@ } }, "Parameters": { - "AssetParameters18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8S3BucketDA18872A": { + "AssetParameters138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5eS3BucketC954C005": { "Type": "String", - "Description": "S3 bucket for asset \"18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8\"" + "Description": "S3 bucket for asset \"138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5e\"" }, - "AssetParameters18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8S3VersionKey8DEA118B": { + "AssetParameters138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5eS3VersionKey2D066AE2": { "Type": "String", - "Description": "S3 key for asset version \"18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8\"" + "Description": "S3 key for asset version \"138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5e\"" }, - "AssetParameters18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8ArtifactHash8C68AE30": { + "AssetParameters138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5eArtifactHash5852F39A": { "Type": "String", - "Description": "Artifact hash for asset \"18da46d8773b50c4a62f9c3c9acf80991f578d190ed091a3ced48ba9fb3d98f8\"" + "Description": "Artifact hash for asset \"138d5170d70070f442a09ab6b7c09fecfa7278d8b8c0e64fdc2addf284172a5e\"" } }, "Outputs": {