Skip to content

Commit

Permalink
feat(aws-lambda-python): add command hooks for bundling to allow for …
Browse files Browse the repository at this point in the history
…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*
  • Loading branch information
webratz committed Dec 14, 2022
1 parent 6e4b4ea commit d3d071f
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 1 deletion.
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$/),
],
}),
}));
});

0 comments on commit d3d071f

Please sign in to comment.