Skip to content

Commit 53beeae

Browse files
authored
feat(lambda-python): add optional poetry bundling exclusion list parameter (#23670)
A summary of this change is: change from use of `cp` to `rsync --exclude='x'` in the bundling commands for `poetry` based lambdas. The intention of this PR is to enable the bundling code for Poetry projects to exclude certain files and/or folders from the bundled assets. Currently, if developing a python lambda using either `virtualenv` itself or a toolchain that leverages virtual environments (re: `poetry`, specifically with `virtualenv.in-project = true`, which is strongly recommended for leveraging python tools in VSCode), the bundling code will copy the entire folder passed in. This leads to copying the entire `.venv` directory into the bundled assets, even though the directory is ignored. Ultimately this leads to inflating the assets by the size of unzipped dependencies (`numpy`, for instance, is 50Mb by itself). I verified this concept works in another project I maintain which leverages `@aws-cdk/aws-lambda-python-alpha` by manually editing the bundling file (I mentioned it in more detail in the linked issue #22585), but this temporary approach requires manually editing files from `node_modules`, so it is not a proper fix. fixes #22585 ---- ### 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 * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? ~~**Note**: I was unable to implement an integration test. My plan was to verify an asset was ignored from the existing poetry integration test sample directory `packages/@aws-cdk/aws-lambda-python/test/lambda-handler-poetry` by passing in `['.ignorefile']` and confirming that asset was excluded, but I was unable to get the test working due to SSM parameters missing. I wasn't sure if I could bootstrap this and get it working.~~ *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 75eb933 commit 53beeae

File tree

38 files changed

+7855
-2570
lines changed

38 files changed

+7855
-2570
lines changed

Diff for: packages/@aws-cdk/aws-lambda-python/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ Packaging is executed using the `Packaging` class, which:
109109
├── poetry.lock # your poetry lock file has to be present at the entry path
110110
```
111111

112+
**Excluding source files**
113+
114+
You can exclude files from being copied using the optional bundling string array parameter `assetExcludes`
115+
116+
```ts
117+
new python.PythonFunction(this, 'function', {
118+
entry: '/path/to/poetry-function',
119+
runtime: Runtime.PYTHON_3_8,
120+
bundling: {
121+
// translates to `rsync --exclude='.venv'`
122+
assetExcludes: ['.venv'],
123+
},
124+
});
125+
```
126+
127+
112128
## Custom Bundling
113129

114130
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:

Diff for: packages/@aws-cdk/aws-lambda-python/lib/bundling.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export class Bundling implements CdkBundlingOptions {
8383
image,
8484
poetryIncludeHashes,
8585
commandHooks,
86+
assetExcludes = [],
8687
} = props;
8788

8889
const outputPath = path.posix.join(AssetStaging.BUNDLING_OUTPUT_DIR, outputPathSuffix);
@@ -93,6 +94,7 @@ export class Bundling implements CdkBundlingOptions {
9394
outputDir: outputPath,
9495
poetryIncludeHashes,
9596
commandHooks,
97+
assetExcludes,
9698
});
9799

98100
this.image = image ?? DockerImage.fromBuild(path.join(__dirname, '../lib'), {
@@ -118,7 +120,10 @@ export class Bundling implements CdkBundlingOptions {
118120
const packaging = Packaging.fromEntry(options.entry, options.poetryIncludeHashes);
119121
let bundlingCommands: string[] = [];
120122
bundlingCommands.push(...options.commandHooks?.beforeBundling(options.inputDir, options.outputDir) ?? []);
121-
bundlingCommands.push(`cp -rTL ${options.inputDir}/ ${options.outputDir}`);
123+
const exclusionStr = options.assetExcludes?.map(item => `--exclude='${item}'`).join(' ');
124+
bundlingCommands.push([
125+
'rsync', '-rLv', exclusionStr ?? '', `${options.inputDir}/`, options.outputDir,
126+
].filter(item => item).join(' '));
122127
bundlingCommands.push(`cd ${options.outputDir}`);
123128
bundlingCommands.push(packaging.exportCommand ?? '');
124129
if (packaging.dependenciesFile) {
@@ -133,8 +138,9 @@ interface BundlingCommandOptions {
133138
readonly entry: string;
134139
readonly inputDir: string;
135140
readonly outputDir: string;
141+
readonly assetExcludes?: string[];
136142
readonly poetryIncludeHashes?: boolean;
137-
readonly commandHooks?: ICommandHooks
143+
readonly commandHooks?: ICommandHooks;
138144
}
139145

140146
/**

Diff for: packages/@aws-cdk/aws-lambda-python/lib/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ export interface BundlingOptions extends DockerRunOptions {
1515
*/
1616
readonly poetryIncludeHashes?: boolean;
1717

18+
/**
19+
* List of file patterns to exclude when copying assets from source for bundling.
20+
*
21+
* @default - Empty list
22+
*/
23+
readonly assetExcludes?: string[];
24+
1825
/**
1926
* Output path suffix: the suffix for the directory into which the bundled output is written.
2027
*

Diff for: packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts

+123-9
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ test('Bundling a function without dependencies', () => {
3737
bundling: expect.objectContaining({
3838
command: [
3939
'bash', '-c',
40-
'cp -rTL /asset-input/ /asset-output && cd /asset-output',
40+
'rsync -rLv /asset-input/ /asset-output && cd /asset-output',
4141
],
4242
}),
4343
}));
@@ -66,7 +66,32 @@ test('Bundling a function with requirements.txt', () => {
6666
bundling: expect.objectContaining({
6767
command: [
6868
'bash', '-c',
69-
'cp -rTL /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
69+
'rsync -rLv /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
70+
],
71+
}),
72+
}));
73+
74+
const files = fs.readdirSync(assetCode.path);
75+
expect(files).toContain('index.py');
76+
expect(files).toContain('requirements.txt');
77+
expect(files).toContain('.ignorelist');
78+
});
79+
80+
test('Bundling a function with requirements.txt using assetExcludes', () => {
81+
const entry = path.join(__dirname, 'lambda-handler');
82+
const assetCode = Bundling.bundle({
83+
entry: entry,
84+
runtime: Runtime.PYTHON_3_7,
85+
architecture: Architecture.X86_64,
86+
assetExcludes: ['.ignorelist'],
87+
});
88+
89+
// Correctly bundles
90+
expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
91+
bundling: expect.objectContaining({
92+
command: [
93+
'bash', '-c',
94+
"rsync -rLv --exclude='.ignorelist' /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output",
7095
],
7196
}),
7297
}));
@@ -89,7 +114,26 @@ test('Bundling Python 2.7 with requirements.txt installed', () => {
89114
bundling: expect.objectContaining({
90115
command: [
91116
'bash', '-c',
92-
'cp -rTL /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
117+
'rsync -rLv /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
118+
],
119+
}),
120+
}));
121+
});
122+
123+
test('Bundling Python 2.7 with requirements.txt installed', () => {
124+
const entry = path.join(__dirname, 'lambda-handler');
125+
Bundling.bundle({
126+
entry: entry,
127+
runtime: Runtime.PYTHON_2_7,
128+
architecture: Architecture.X86_64,
129+
});
130+
131+
// Correctly bundles with requirements.txt pip installed
132+
expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
133+
bundling: expect.objectContaining({
134+
command: [
135+
'bash', '-c',
136+
'rsync -rLv /asset-input/ /asset-output && cd /asset-output && python -m pip install -r requirements.txt -t /asset-output',
93137
],
94138
}),
95139
}));
@@ -109,7 +153,7 @@ test('Bundling a layer with dependencies', () => {
109153
bundling: expect.objectContaining({
110154
command: [
111155
'bash', '-c',
112-
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python && python -m pip install -r requirements.txt -t /asset-output/python',
156+
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python && python -m pip install -r requirements.txt -t /asset-output/python',
113157
],
114158
}),
115159
}));
@@ -129,7 +173,7 @@ test('Bundling a python code layer', () => {
129173
bundling: expect.objectContaining({
130174
command: [
131175
'bash', '-c',
132-
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python',
176+
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python',
133177
],
134178
}),
135179
}));
@@ -149,7 +193,35 @@ test('Bundling a function with pipenv dependencies', () => {
149193
bundling: expect.objectContaining({
150194
command: [
151195
'bash', '-c',
152-
'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',
196+
'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',
197+
],
198+
}),
199+
}));
200+
201+
const files = fs.readdirSync(assetCode.path);
202+
expect(files).toContain('index.py');
203+
expect(files).toContain('Pipfile');
204+
expect(files).toContain('Pipfile.lock');
205+
// Contains hidden files.
206+
expect(files).toContain('.ignorefile');
207+
});
208+
209+
test('Bundling a function with pipenv dependencies with assetExcludes', () => {
210+
const entry = path.join(__dirname, 'lambda-handler-pipenv');
211+
212+
const assetCode = Bundling.bundle({
213+
entry: path.join(entry, '.'),
214+
runtime: Runtime.PYTHON_3_9,
215+
architecture: Architecture.X86_64,
216+
outputPathSuffix: 'python',
217+
assetExcludes: ['.ignorefile'],
218+
});
219+
220+
expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
221+
bundling: expect.objectContaining({
222+
command: [
223+
'bash', '-c',
224+
"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",
153225
],
154226
}),
155227
}));
@@ -176,7 +248,7 @@ test('Bundling a function with poetry dependencies', () => {
176248
bundling: expect.objectContaining({
177249
command: [
178250
'bash', '-c',
179-
'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',
251+
'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',
180252
],
181253
}),
182254
}));
@@ -189,6 +261,48 @@ test('Bundling a function with poetry dependencies', () => {
189261
expect(files).toContain('.ignorefile');
190262
});
191263

264+
test('Bundling a function with poetry and assetExcludes', () => {
265+
const entry = path.join(__dirname, 'lambda-handler-poetry');
266+
267+
Bundling.bundle({
268+
entry: path.join(entry, '.'),
269+
runtime: Runtime.PYTHON_3_9,
270+
architecture: Architecture.X86_64,
271+
outputPathSuffix: 'python',
272+
assetExcludes: ['.ignorefile'],
273+
});
274+
275+
expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
276+
bundling: expect.objectContaining({
277+
command: [
278+
'bash', '-c',
279+
"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",
280+
],
281+
}),
282+
}));
283+
284+
});
285+
286+
test('Bundling a function with poetry and no assetExcludes', () => {
287+
const entry = path.join(__dirname, 'lambda-handler-poetry');
288+
289+
Bundling.bundle({
290+
entry: path.join(entry, '.'),
291+
runtime: Runtime.PYTHON_3_9,
292+
architecture: Architecture.X86_64,
293+
outputPathSuffix: 'python',
294+
});
295+
296+
expect(Code.fromAsset).toHaveBeenCalledWith(entry, expect.objectContaining({
297+
bundling: expect.objectContaining({
298+
command: [
299+
'bash', '-c',
300+
expect.not.stringContaining('--exclude'),
301+
],
302+
}),
303+
}));
304+
});
305+
192306
test('Bundling a function with poetry dependencies, with hashes', () => {
193307
const entry = path.join(__dirname, 'lambda-handler-poetry');
194308

@@ -204,7 +318,7 @@ test('Bundling a function with poetry dependencies, with hashes', () => {
204318
bundling: expect.objectContaining({
205319
command: [
206320
'bash', '-c',
207-
'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',
321+
'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',
208322
],
209323
}),
210324
}));
@@ -234,7 +348,7 @@ test('Bundling a function with custom bundling image', () => {
234348
image,
235349
command: [
236350
'bash', '-c',
237-
'cp -rTL /asset-input/ /asset-output/python && cd /asset-output/python && python -m pip install -r requirements.txt -t /asset-output/python',
351+
'rsync -rLv /asset-input/ /asset-output/python && cd /asset-output/python && python -m pip install -r requirements.txt -t /asset-output/python',
238352
],
239353
}),
240354
}));

0 commit comments

Comments
 (0)