diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md index fc68cbce9d837..04f092e5f2875 100644 --- a/packages/@aws-cdk/aws-lambda-python/README.md +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -198,3 +198,39 @@ new python.PythonFunction(this, 'function', { }, }); ``` + +## Command hooks + +It is possible to run additional commands by specifying the `commandHooks` prop: + +```ts +const entry = '/path/to/function'; +new python.PythonFunction(this, 'function', { + entry, + runtime: Runtime.PYTHON_3_8, + bundling: { + commandHooks: { + // run tests + beforeBundling(inputDir: string): string[] { + return ['pytest']; + }, + afterBundling(inputDir: string): string[] { + return ['pylint']; + }, + // ... + }, + }, +}); +``` + +The following hooks are available: + +- `beforeBundling`: runs before all bundling commands +- `afterBundling`: runs after all bundling commands + +They all receive the directory containing the dependencies file (`inputDir`) and the +directory where the bundled asset will be output (`outputDir`). They must return +an array of commands to run. Commands are chained with `&&`. + +The commands will run in the environment in which bundling occurs: inside the +container for Docker bundling or on the host OS for local bundling. diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts index 726b72c5bc2b8..5302673a5e296 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { Architecture, AssetCode, Code, Runtime } from '@aws-cdk/aws-lambda'; import { AssetStaging, BundlingOptions as CdkBundlingOptions, DockerImage } from '@aws-cdk/core'; import { Packaging, DependenciesFile } from './packaging'; -import { BundlingOptions } from './types'; +import { BundlingOptions, ICommandHooks } from './types'; /** * Dependency files to exclude from the asset hash. @@ -68,6 +68,7 @@ export class Bundling implements CdkBundlingOptions { outputPathSuffix = '', image, poetryIncludeHashes, + commandHooks, } = props; const outputPath = path.posix.join(AssetStaging.BUNDLING_OUTPUT_DIR, outputPathSuffix); @@ -77,6 +78,7 @@ export class Bundling implements CdkBundlingOptions { inputDir: AssetStaging.BUNDLING_INPUT_DIR, outputDir: outputPath, poetryIncludeHashes, + commandHooks, }); this.image = image ?? DockerImage.fromBuild(path.join(__dirname, '../lib'), { @@ -93,12 +95,14 @@ export class Bundling implements CdkBundlingOptions { private createBundlingCommand(options: BundlingCommandOptions): string[] { const packaging = Packaging.fromEntry(options.entry, options.poetryIncludeHashes); let bundlingCommands: string[] = []; + bundlingCommands.push(...options.commandHooks?.beforeBundling(options.inputDir, options.outputDir) ?? []); bundlingCommands.push(`cp -rTL ${options.inputDir}/ ${options.outputDir}`); bundlingCommands.push(`cd ${options.outputDir}`); bundlingCommands.push(packaging.exportCommand ?? ''); if (packaging.dependenciesFile) { bundlingCommands.push(`python -m pip install -r ${DependenciesFile.PIP} -t ${options.outputDir}`); } + bundlingCommands.push(...options.commandHooks?.afterBundling(options.inputDir, options.outputDir) ?? []); return bundlingCommands; } } @@ -108,6 +112,7 @@ interface BundlingCommandOptions { readonly inputDir: string; readonly outputDir: string; readonly poetryIncludeHashes?: boolean; + readonly commandHooks?: ICommandHooks } /** diff --git a/packages/@aws-cdk/aws-lambda-python/lib/types.ts b/packages/@aws-cdk/aws-lambda-python/lib/types.ts index 3689c43335959..ff9608b97e2f1 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/types.ts @@ -86,4 +86,45 @@ export interface BundlingOptions { * @default - Based on `assetHashType` */ readonly assetHash?: string; + + /** + * Command hooks + * + * @default - do not run additional commands + */ + readonly commandHooks?: ICommandHooks; +} + +/** + * Command hooks + * + * These commands will run in the environment in which bundling occurs: inside + * the container for Docker bundling or on the host OS for local bundling. + * + * Commands are chained with `&&`. + * + * ```text + * { + * // Run tests prior to bundling + * beforeBundling(inputDir: string, outputDir: string): string[] { + * return [`pytest`]; + * } + * // ... + * } + * ``` + */ +export interface ICommandHooks { + /** + * Returns commands to run before bundling. + * + * Commands are chained with `&&`. + */ + beforeBundling(inputDir: string, outputDir: string): string[]; + + /** + * Returns commands to run after bundling. + * + * Commands are chained with `&&`. + */ + afterBundling(inputDir: string, outputDir: string): string[]; } diff --git a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts index 5626a3f02d2a4..402953c197c1c 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -299,3 +299,32 @@ test('Build docker image when bundling is not skipped', () => { expect(DockerImage.fromBuild).toHaveBeenCalled(); }); + +test('with command hooks', () => { + const entry = path.join(__dirname, 'lambda-handler'); + Bundling.bundle({ + entry: entry, + runtime: Runtime.PYTHON_3_7, + skip: false, + commandHooks: { + beforeBundling(inputDir: string, outputDir: string): string[] { + return [ + `echo hello > ${inputDir}/a.txt`, + `cp ${inputDir}/a.txt ${outputDir}`, + ]; + }, + afterBundling(inputDir: string, outputDir: string): string[] { + return [`cp ${inputDir}/b.txt ${outputDir}/txt`]; + }, + }, + }); + + expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({ + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + expect.stringMatching(/^echo hello > \/asset-input\/a.txt && cp \/asset-input\/a.txt \/asset-output && .+ && cp \/asset-input\/b.txt \/asset-output\/txt$/), + ], + }), + })); +});