From 02a5482263b993e02c57923bda5e186d72255ade Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:53:20 -0400 Subject: [PATCH] fix(synthetics): asset code validation failed on bundled assets (#26291) A re-roll of #19342. Thanks @RichiCoder1 for doing most of this work! This PR moves asset validation from _before_ staging the asset to _after_, and then validates on the staged asset instead. This allows for asset bundling because our prior validation was too eager. In addition, this construct can help with synthetic canaries + bundled code: https://github.com/mrgrain/cdk-esbuild#amazon-cloudwatch-synthetics-canary-monitoring. Fixes #11630 Co-authored-by: Richard Simpson richicoder1@outlook.com ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-synthetics-alpha/lib/code.ts | 25 +++++---- .../aws-synthetics-alpha/test/code.test.ts | 52 ++++++++++++++++++- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-synthetics-alpha/lib/code.ts b/packages/@aws-cdk/aws-synthetics-alpha/lib/code.ts index 31387b5aadee8..c8cfb405a4b9c 100644 --- a/packages/@aws-cdk/aws-synthetics-alpha/lib/code.ts +++ b/packages/@aws-cdk/aws-synthetics-alpha/lib/code.ts @@ -4,6 +4,7 @@ import * as s3 from 'aws-cdk-lib/aws-s3'; import * as s3_assets from 'aws-cdk-lib/aws-s3-assets'; import { Construct } from 'constructs'; import { RuntimeFamily } from './runtime'; +import { Stage } from 'aws-cdk-lib/core'; /** * The code the canary should execute @@ -97,8 +98,6 @@ export class AssetCode extends Code { } public bind(scope: Construct, handler: string, family: RuntimeFamily): CodeConfig { - this.validateCanaryAsset(handler, family); - // If the same AssetCode is used multiple times, retain only the first instantiation. if (!this.asset) { this.asset = new s3_assets.Asset(scope, 'Code', { @@ -107,6 +106,8 @@ export class AssetCode extends Code { }); } + this.validateCanaryAsset(scope, handler, family); + return { s3Location: { bucketName: this.asset.s3BucketName, @@ -124,21 +125,27 @@ export class AssetCode extends Code { * Requires canary file to be directly inside node_modules folder. * Requires canary file name matches the handler name. * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html - * - * @param handler the canary handler */ - private validateCanaryAsset(handler: string, family: RuntimeFamily) { - if (path.extname(this.assetPath) !== '.zip') { - if (!fs.lstatSync(this.assetPath).isDirectory()) { + private validateCanaryAsset(scope: Construct, handler: string, family: RuntimeFamily) { + if (!this.asset) { + throw new Error("'validateCanaryAsset' must be called after 'this.asset' is instantiated"); + } + + // Get the staged (or copied) asset path. + const assetOutDir = Stage.of(scope)?.assetOutdir; + const assetPath = assetOutDir ? path.join(assetOutDir, this.asset.assetPath): this.assetPath; + + if (path.extname(assetPath) !== '.zip') { + if (!fs.lstatSync(assetPath).isDirectory()) { throw new Error(`Asset must be a .zip file or a directory (${this.assetPath})`); } const filename = handler.split('.')[0]; const nodeFilename = `${filename}.js`; const pythonFilename = `${filename}.py`; - if (family === RuntimeFamily.NODEJS && !fs.existsSync(path.join(this.assetPath, 'nodejs', 'node_modules', nodeFilename))) { + if (family === RuntimeFamily.NODEJS && !fs.existsSync(path.join(assetPath, 'nodejs', 'node_modules', nodeFilename))) { throw new Error(`The canary resource requires that the handler is present at "nodejs/node_modules/${nodeFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); } - if (family === RuntimeFamily.PYTHON && !fs.existsSync(path.join(this.assetPath, 'python', pythonFilename))) { + if (family === RuntimeFamily.PYTHON && !fs.existsSync(path.join(assetPath, 'python', pythonFilename))) { throw new Error(`The canary resource requires that the handler is present at "python/${pythonFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`); } } diff --git a/packages/@aws-cdk/aws-synthetics-alpha/test/code.test.ts b/packages/@aws-cdk/aws-synthetics-alpha/test/code.test.ts index 9eef5059c9ad6..396d245d2bd98 100644 --- a/packages/@aws-cdk/aws-synthetics-alpha/test/code.test.ts +++ b/packages/@aws-cdk/aws-synthetics-alpha/test/code.test.ts @@ -1,7 +1,8 @@ +import * as fs from 'fs'; import * as path from 'path'; import { Template } from 'aws-cdk-lib/assertions'; import * as s3 from 'aws-cdk-lib/aws-s3'; -import { App, Stack } from 'aws-cdk-lib'; +import { App, Stack, DockerImage } from 'aws-cdk-lib'; import * as cxapi from 'aws-cdk-lib/cx-api'; import * as synthetics from '../lib'; import { RuntimeFamily } from '../lib'; @@ -167,6 +168,55 @@ describe(synthetics.Code.fromAsset, () => { expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'incorrect.handler', synthetics.RuntimeFamily.NODEJS)) .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/incorrect.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); }); + + test('passes if bundling is specified', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + // WHEN + const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules'); + const code = synthetics.Code.fromAsset(assetPath, { + bundling: { + image: DockerImage.fromRegistry('dummy'), + local: { + tryBundle(outputDir) { + const stageDir = path.join(outputDir, 'nodejs', 'node_modules'); + fs.mkdirSync(path.join(outputDir, 'nodejs')); + fs.mkdirSync(stageDir); + fs.copyFileSync(path.join(assetPath, 'canary.js'), path.join(stageDir, 'canary.js')); + return true; + }, + }, + }, + }); + + // THEN + expect(() => code.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS)) + .not.toThrow(); + }); + + test('fails if bundling is specified but folder structure is wrong', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + // WHEN + const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules'); + const code = synthetics.Code.fromAsset(assetPath, { + bundling: { + image: DockerImage.fromRegistry('dummy'), + local: { + tryBundle(outputDir) { + fs.copyFileSync(path.join(assetPath, 'canary.js'), path.join(outputDir, 'canary.js')); + return true; + }, + }, + }, + }); + + // THEN + expect(() => code.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS)) + .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/canary.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); + }); }); describe(synthetics.Code.fromBucket, () => {