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(lambda-python): add optional poetry bundling exclusion list parameter #23670

Merged
16 changes: 16 additions & 0 deletions packages/@aws-cdk/aws-lambda-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ Packaging is executed using the `Packaging` class, which:
├── poetry.lock # your poetry lock file has to be present at the entry path
```

**Excluding source files**

You can exclude files from being copied using the optional bundling string array parameter `assetExcludes`

```ts
new python.PythonFunction(this, 'function', {
entry: '/path/to/poetry-function',
runtime: Runtime.PYTHON_3_8,
bundling: {
// translates to `rsync --exclude='.venv'`
assetExcludes: ['.venv'],
},
});
```


## Custom Bundling

Custom bundling can be performed by passing in additional build arguments that point to index URLs to private repos, or by using an entirely custom Docker images for bundling dependencies. The build args currently supported are:
Expand Down
10 changes: 8 additions & 2 deletions packages/@aws-cdk/aws-lambda-python/lib/bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class Bundling implements CdkBundlingOptions {
image,
poetryIncludeHashes,
commandHooks,
assetExcludes = [],
} = props;

const outputPath = path.posix.join(AssetStaging.BUNDLING_OUTPUT_DIR, outputPathSuffix);
Expand All @@ -93,6 +94,7 @@ export class Bundling implements CdkBundlingOptions {
outputDir: outputPath,
poetryIncludeHashes,
commandHooks,
assetExcludes,
});

this.image = image ?? DockerImage.fromBuild(path.join(__dirname, '../lib'), {
Expand All @@ -118,7 +120,10 @@ export class Bundling implements CdkBundlingOptions {
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}`);
const exclusionStr = options.assetExcludes?.map(item => `--exclude='${item}'`).join(' ');
bundlingCommands.push([
'rsync', '-rLv', exclusionStr ?? '', `${options.inputDir}/`, options.outputDir,
].filter(item => item).join(' '));
bundlingCommands.push(`cd ${options.outputDir}`);
bundlingCommands.push(packaging.exportCommand ?? '');
if (packaging.dependenciesFile) {
Expand All @@ -133,8 +138,9 @@ interface BundlingCommandOptions {
readonly entry: string;
readonly inputDir: string;
readonly outputDir: string;
readonly assetExcludes?: string[];
readonly poetryIncludeHashes?: boolean;
readonly commandHooks?: ICommandHooks
readonly commandHooks?: ICommandHooks;
}

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/@aws-cdk/aws-lambda-python/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export interface BundlingOptions extends DockerRunOptions {
*/
readonly poetryIncludeHashes?: boolean;

/**
* List of file patterns to exclude when copying assets from source for bundling.
*
* @default - Empty list
*/
readonly assetExcludes?: string[];

/**
* Output path suffix: the suffix for the directory into which the bundled output is written.
*
Expand Down
132 changes: 123 additions & 9 deletions packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test('Bundling a function without dependencies', () => {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output && cd /asset-output',
'rsync -rLv /asset-input/ /asset-output && cd /asset-output',
],
}),
}));
Expand Down Expand Up @@ -66,7 +66,32 @@ test('Bundling a function with requirements.txt', () => {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
'rsync -rLv /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
],
}),
}));

const files = fs.readdirSync(assetCode.path);
expect(files).toContain('index.py');
expect(files).toContain('requirements.txt');
expect(files).toContain('.ignorelist');
});

test('Bundling a function with requirements.txt using assetExcludes', () => {
const entry = path.join(__dirname, 'lambda-handler');
const assetCode = Bundling.bundle({
entry: entry,
runtime: Runtime.PYTHON_3_7,
architecture: Architecture.X86_64,
assetExcludes: ['.ignorelist'],
});

// Correctly bundles
expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
bundling: expect.objectContaining({
command: [
'bash', '-c',
"rsync -rLv --exclude='.ignorelist' /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output",
],
}),
}));
Expand All @@ -89,7 +114,26 @@ test('Bundling Python 2.7 with requirements.txt installed', () => {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
'rsync -rLv /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
],
}),
}));
});

test('Bundling Python 2.7 with requirements.txt installed', () => {
const entry = path.join(__dirname, 'lambda-handler');
Bundling.bundle({
entry: entry,
runtime: Runtime.PYTHON_2_7,
architecture: Architecture.X86_64,
});

// Correctly bundles with requirements.txt pip installed
expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
bundling: expect.objectContaining({
command: [
'bash', '-c',
'rsync -rLv /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
],
}),
}));
Expand All @@ -109,7 +153,7 @@ test('Bundling a layer with dependencies', () => {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python && python -m pip install -r requirements.txt -t /asset-output/python',
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python && python -m pip install -r requirements.txt -t /asset-output/python',
],
}),
}));
Expand All @@ -129,7 +173,7 @@ test('Bundling a python code layer', () => {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python',
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python',
],
}),
}));
Expand All @@ -149,7 +193,35 @@ test('Bundling a function with pipenv dependencies', () => {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python && PIPENV_VENV_IN_PROJECT=1 pipenv lock -r > requirements.txt && rm -rf .venv && python -m pip install -r requirements.txt -t /asset-output/python',
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python && PIPENV_VENV_IN_PROJECT=1 pipenv lock -r > requirements.txt && rm -rf .venv && python -m pip install -r requirements.txt -t /asset-output/python',
],
}),
}));

const files = fs.readdirSync(assetCode.path);
expect(files).toContain('index.py');
expect(files).toContain('Pipfile');
expect(files).toContain('Pipfile.lock');
// Contains hidden files.
expect(files).toContain('.ignorefile');
});

test('Bundling a function with pipenv dependencies with assetExcludes', () => {
const entry = path.join(__dirname, 'lambda-handler-pipenv');

const assetCode = Bundling.bundle({
entry: path.join(entry, '.'),
runtime: Runtime.PYTHON_3_9,
architecture: Architecture.X86_64,
outputPathSuffix: 'python',
assetExcludes: ['.ignorefile'],
});

expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
bundling: expect.objectContaining({
command: [
'bash', '-c',
"rsync -rLv --exclude='.ignorefile' /asset-input/ /asset-output/python && cd /asset-output/python && PIPENV_VENV_IN_PROJECT=1 pipenv lock -r > requirements.txt && rm -rf .venv && python -m pip install -r requirements.txt -t /asset-output/python",
],
}),
}));
Expand All @@ -176,7 +248,7 @@ test('Bundling a function with poetry dependencies', () => {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python && poetry export --without-hashes --with-credentials --format requirements.txt --output requirements.txt && python -m pip install -r requirements.txt -t /asset-output/python',
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python && poetry export --without-hashes --with-credentials --format requirements.txt --output requirements.txt && python -m pip install -r requirements.txt -t /asset-output/python',
],
}),
}));
Expand All @@ -189,6 +261,48 @@ test('Bundling a function with poetry dependencies', () => {
expect(files).toContain('.ignorefile');
});

test('Bundling a function with poetry and assetExcludes', () => {
const entry = path.join(__dirname, 'lambda-handler-poetry');

Bundling.bundle({
entry: path.join(entry, '.'),
runtime: Runtime.PYTHON_3_9,
architecture: Architecture.X86_64,
outputPathSuffix: 'python',
assetExcludes: ['.ignorefile'],
});

expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
bundling: expect.objectContaining({
command: [
'bash', '-c',
"rsync -rLv --exclude='.ignorefile' /asset-input/ /asset-output/python && cd /asset-output/python && poetry export --without-hashes --with-credentials --format requirements.txt --output requirements.txt && python -m pip install -r requirements.txt -t /asset-output/python",
],
}),
}));

});

test('Bundling a function with poetry and no assetExcludes', () => {
const entry = path.join(__dirname, 'lambda-handler-poetry');

Bundling.bundle({
entry: path.join(entry, '.'),
runtime: Runtime.PYTHON_3_9,
architecture: Architecture.X86_64,
outputPathSuffix: 'python',
});

expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
bundling: expect.objectContaining({
command: [
'bash', '-c',
expect.not.stringContaining('--exclude'),
],
}),
}));
});

test('Bundling a function with poetry dependencies, with hashes', () => {
const entry = path.join(__dirname, 'lambda-handler-poetry');

Expand All @@ -204,7 +318,7 @@ test('Bundling a function with poetry dependencies, with hashes', () => {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python && poetry export --with-credentials --format requirements.txt --output requirements.txt && python -m pip install -r requirements.txt -t /asset-output/python',
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python && poetry export --with-credentials --format requirements.txt --output requirements.txt && python -m pip install -r requirements.txt -t /asset-output/python',
],
}),
}));
Expand Down Expand Up @@ -234,7 +348,7 @@ test('Bundling a function with custom bundling image', () => {
image,
command: [
'bash', '-c',
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python && python -m pip install -r requirements.txt -t /asset-output/python',
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python && python -m pip install -r requirements.txt -t /asset-output/python',
],
}),
}));
Expand Down
Loading