Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws-lambda-python): add command hooks for bundling to allow for execution of custom commands in the build container #23330

Merged
merged 3 commits into from Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/@aws-cdk/aws-lambda-python/README.md
Expand Up @@ -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.
7 changes: 6 additions & 1 deletion packages/@aws-cdk/aws-lambda-python/lib/bundling.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -68,6 +68,7 @@ export class Bundling implements CdkBundlingOptions {
outputPathSuffix = '',
image,
poetryIncludeHashes,
commandHooks,
} = props;

const outputPath = path.posix.join(AssetStaging.BUNDLING_OUTPUT_DIR, outputPathSuffix);
Expand All @@ -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'), {
Expand All @@ -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;
}
}
Expand All @@ -108,6 +112,7 @@ interface BundlingCommandOptions {
readonly inputDir: string;
readonly outputDir: string;
readonly poetryIncludeHashes?: boolean;
readonly commandHooks?: ICommandHooks
}

/**
Expand Down
41 changes: 41 additions & 0 deletions packages/@aws-cdk/aws-lambda-python/lib/types.ts
Expand Up @@ -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[];
}
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts
Expand Up @@ -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$/),
],
}),
}));
});