Skip to content

Commit

Permalink
fix(synthetics): asset code validation failed on bundled assets (#26291)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
kaizencc committed Jul 10, 2023
1 parent bd06669 commit 02a5482
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 10 deletions.
25 changes: 16 additions & 9 deletions packages/@aws-cdk/aws-synthetics-alpha/lib/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', {
Expand All @@ -107,6 +106,8 @@ export class AssetCode extends Code {
});
}

this.validateCanaryAsset(scope, handler, family);

return {
s3Location: {
bucketName: this.asset.s3BucketName,
Expand All @@ -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)`);
}
}
Expand Down
52 changes: 51 additions & 1 deletion packages/@aws-cdk/aws-synthetics-alpha/test/code.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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, () => {
Expand Down

0 comments on commit 02a5482

Please sign in to comment.