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": {