From d3d071f51fab61ae0e484f143e68e698bba48537 Mon Sep 17 00:00:00 2001 From: Andreas Sieferlinger Date: Wed, 14 Dec 2022 13:12:43 +0100 Subject: [PATCH] feat(aws-lambda-python): add command hooks for bundling to allow for execution of custom commands in the build container (#23330) This brings `aws-lambda-python` the same functionality as the `go` and `nodejs` lambda functions which allows to execute custom commands before and after installing the dependencies within the bundling environment (container). This can be useful to run test and linters or add additional files. Did not add an integration test, as there seemed to be none for the other packages either. ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Construct Runtime Dependencies: * [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-construct-runtime-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda-python/README.md | 36 ++++++++++++++++ .../aws-lambda-python/lib/bundling.ts | 7 +++- .../@aws-cdk/aws-lambda-python/lib/types.ts | 41 +++++++++++++++++++ .../aws-lambda-python/test/bundling.test.ts | 29 +++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) 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$/), + ], + }), + })); +});