Skip to content

Commit cf1ba8e

Browse files
authored
Merge branch 'master' into bump-cfnspec/v29.0.0
2 parents 21cf749 + 37debc0 commit cf1ba8e

14 files changed

+328
-189
lines changed

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

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,56 @@ export interface BundlingOptions {
3131
* Output path suffix ('python' for a layer, '.' otherwise)
3232
*/
3333
readonly outputPathSuffix: string;
34+
35+
/**
36+
* Determines how asset hash is calculated. Assets will get rebuild and
37+
* uploaded only if their hash has changed.
38+
*
39+
* If asset hash is set to `SOURCE` (default), then only changes to the source
40+
* directory will cause the asset to rebuild. This means, for example, that in
41+
* order to pick up a new dependency version, a change must be made to the
42+
* source tree. Ideally, this can be implemented by including a dependency
43+
* lockfile in your source tree or using fixed dependencies.
44+
*
45+
* If the asset hash is set to `OUTPUT`, the hash is calculated after
46+
* bundling. This means that any change in the output will cause the asset to
47+
* be invalidated and uploaded. Bear in mind that `pip` adds timestamps to
48+
* dependencies it installs, which implies that in this mode Python bundles
49+
* will _always_ get rebuild and uploaded. Normally this is an anti-pattern
50+
* since build
51+
*
52+
* @default AssetHashType.SOURCE By default, hash is calculated based on the
53+
* contents of the source directory. If `assetHash` is also specified, the
54+
* default is `CUSTOM`. This means that only updates to the source will cause
55+
* the asset to rebuild.
56+
*/
57+
readonly assetHashType?: cdk.AssetHashType;
58+
59+
/**
60+
* Specify a custom hash for this asset. If `assetHashType` is set it must
61+
* be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will
62+
* be SHA256 hashed and encoded as hex. The resulting hash will be the asset
63+
* hash.
64+
*
65+
* NOTE: the hash is used in order to identify a specific revision of the asset, and
66+
* used for optimizing and caching deployment activities related to this asset such as
67+
* packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will
68+
* need to make sure it is updated every time the asset changes, or otherwise it is
69+
* possible that some deployments will not be invalidated.
70+
*
71+
* @default - based on `assetHashType`
72+
*/
73+
readonly assetHash?: string;
3474
}
3575

3676
/**
3777
* Produce bundled Lambda asset code
3878
*/
39-
export function bundle(options: BundlingOptions): lambda.AssetCode {
79+
export function bundle(options: BundlingOptions): lambda.Code {
4080
const { entry, runtime, outputPathSuffix } = options;
4181

42-
const hasDeps = hasDependencies(entry);
82+
const stagedir = cdk.FileSystem.mkdtemp('python-bundling-');
83+
const hasDeps = stageDependencies(entry, stagedir);
4384

4485
const depsCommand = chain([
4586
hasDeps ? `rsync -r ${BUNDLER_DEPENDENCIES_CACHE}/. ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}` : '',
@@ -54,15 +95,19 @@ export function bundle(options: BundlingOptions): lambda.AssetCode {
5495
? 'Dockerfile.dependencies'
5596
: 'Dockerfile';
5697

57-
const image = cdk.BundlingDockerImage.fromAsset(entry, {
98+
// copy Dockerfile to workdir
99+
fs.copyFileSync(path.join(__dirname, dockerfile), path.join(stagedir, dockerfile));
100+
101+
const image = cdk.BundlingDockerImage.fromAsset(stagedir, {
58102
buildArgs: {
59103
IMAGE: runtime.bundlingDockerImage.image,
60104
},
61-
file: path.join(__dirname, dockerfile),
105+
file: dockerfile,
62106
});
63107

64108
return lambda.Code.fromAsset(entry, {
65-
assetHashType: cdk.AssetHashType.BUNDLE,
109+
assetHashType: options.assetHashType,
110+
assetHash: options.assetHash,
66111
exclude: DEPENDENCY_EXCLUDES,
67112
bundling: {
68113
image,
@@ -75,20 +120,25 @@ export function bundle(options: BundlingOptions): lambda.AssetCode {
75120
* Checks to see if the `entry` directory contains a type of dependency that
76121
* we know how to install.
77122
*/
78-
export function hasDependencies(entry: string): boolean {
79-
if (fs.existsSync(path.join(entry, 'Pipfile'))) {
80-
return true;
81-
}
82-
83-
if (fs.existsSync(path.join(entry, 'poetry.lock'))) {
84-
return true;
85-
}
86-
87-
if (fs.existsSync(path.join(entry, 'requirements.txt'))) {
88-
return true;
123+
export function stageDependencies(entry: string, stagedir: string): boolean {
124+
const prefixes = [
125+
'Pipfile',
126+
'pyproject',
127+
'poetry',
128+
'requirements.txt',
129+
];
130+
131+
let found = false;
132+
for (const file of fs.readdirSync(entry)) {
133+
for (const prefix of prefixes) {
134+
if (file.startsWith(prefix)) {
135+
fs.copyFileSync(path.join(entry, file), path.join(stagedir, file));
136+
found = true;
137+
}
138+
}
89139
}
90140

91-
return false;
141+
return found;
92142
}
93143

94144
function chain(commands: string[]): string {

packages/@aws-cdk/aws-lambda-python/lib/function.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import * as lambda from '@aws-cdk/aws-lambda';
4+
import { AssetHashType } from '@aws-cdk/core';
45
import { bundle } from './bundling';
56

67
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
@@ -37,6 +38,45 @@ export interface PythonFunctionProps extends lambda.FunctionOptions {
3738
* @default lambda.Runtime.PYTHON_3_7
3839
*/
3940
readonly runtime?: lambda.Runtime;
41+
42+
/**
43+
* Determines how asset hash is calculated. Assets will get rebuild and
44+
* uploaded only if their hash has changed.
45+
*
46+
* If asset hash is set to `SOURCE` (default), then only changes to the source
47+
* directory will cause the asset to rebuild. This means, for example, that in
48+
* order to pick up a new dependency version, a change must be made to the
49+
* source tree. Ideally, this can be implemented by including a dependency
50+
* lockfile in your source tree or using fixed dependencies.
51+
*
52+
* If the asset hash is set to `OUTPUT`, the hash is calculated after
53+
* bundling. This means that any change in the output will cause the asset to
54+
* be invalidated and uploaded. Bear in mind that `pip` adds timestamps to
55+
* dependencies it installs, which implies that in this mode Python bundles
56+
* will _always_ get rebuild and uploaded. Normally this is an anti-pattern
57+
* since build
58+
*
59+
* @default AssetHashType.SOURCE By default, hash is calculated based on the
60+
* contents of the source directory. This means that only updates to the
61+
* source will cause the asset to rebuild.
62+
*/
63+
readonly assetHashType?: AssetHashType;
64+
65+
/**
66+
* Specify a custom hash for this asset. If `assetHashType` is set it must
67+
* be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will
68+
* be SHA256 hashed and encoded as hex. The resulting hash will be the asset
69+
* hash.
70+
*
71+
* NOTE: the hash is used in order to identify a specific revision of the asset, and
72+
* used for optimizing and caching deployment activities related to this asset such as
73+
* packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will
74+
* need to make sure it is updated every time the asset changes, or otherwise it is
75+
* possible that some deployments will not be invalidated.
76+
*
77+
* @default - based on `assetHashType`
78+
*/
79+
readonly assetHash?: string;
4080
}
4181

4282
/**
@@ -70,6 +110,8 @@ export class PythonFunction extends lambda.Function {
70110
runtime,
71111
entry,
72112
outputPathSuffix: '.',
113+
assetHashType: props.assetHashType,
114+
assetHash: props.assetHash,
73115
}),
74116
handler: `${index.slice(0, -3)}.${handler}`,
75117
});

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

Lines changed: 17 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import { Code, Runtime } from '@aws-cdk/aws-lambda';
4-
import { hasDependencies, bundle } from '../lib/bundling';
4+
import { FileSystem } from '@aws-cdk/core';
5+
import { stageDependencies, bundle } from '../lib/bundling';
56

67
jest.mock('@aws-cdk/aws-lambda');
7-
const existsSyncOriginal = fs.existsSync;
8-
const existsSyncMock = jest.spyOn(fs, 'existsSync');
98

109
jest.mock('child_process', () => ({
1110
spawnSync: jest.fn(() => {
@@ -41,9 +40,6 @@ test('Bundling a function without dependencies', () => {
4140
],
4241
}),
4342
}));
44-
45-
// Searches for requirements.txt in entry
46-
expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt'));
4743
});
4844

4945
test('Bundling a function with requirements.txt installed', () => {
@@ -63,9 +59,6 @@ test('Bundling a function with requirements.txt installed', () => {
6359
],
6460
}),
6561
}));
66-
67-
// Searches for requirements.txt in entry
68-
expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt'));
6962
});
7063

7164
test('Bundling Python 2.7 with requirements.txt installed', () => {
@@ -85,9 +78,6 @@ test('Bundling Python 2.7 with requirements.txt installed', () => {
8578
],
8679
}),
8780
}));
88-
89-
// Searches for requirements.txt in entry
90-
expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt'));
9181
});
9282

9383
test('Bundling a layer with dependencies', () => {
@@ -128,42 +118,24 @@ test('Bundling a python code layer', () => {
128118
}));
129119
});
130120

131-
describe('Dependency detection', () => {
132-
test('Detects pipenv', () => {
133-
existsSyncMock.mockImplementation((p: fs.PathLike) => {
134-
if (/Pipfile/.test(p.toString())) {
135-
return true;
136-
}
137-
return existsSyncOriginal(p);
138-
});
139-
140-
expect(hasDependencies('/asset-input')).toEqual(true);
141-
});
142-
143-
test('Detects poetry', () => {
144-
existsSyncMock.mockImplementation((p: fs.PathLike) => {
145-
if (/poetry.lock/.test(p.toString())) {
146-
return true;
147-
}
148-
return existsSyncOriginal(p);
149-
});
150-
151-
expect(hasDependencies('/asset-input')).toEqual(true);
152-
});
153121

154-
test('Detects requirements.txt', () => {
155-
existsSyncMock.mockImplementation((p: fs.PathLike) => {
156-
if (/requirements.txt/.test(p.toString())) {
157-
return true;
158-
}
159-
return existsSyncOriginal(p);
160-
});
161-
162-
expect(hasDependencies('/asset-input')).toEqual(true);
122+
describe('Dependency detection', () => {
123+
test.each(['Pipfile', 'poetry.lock', 'requirements.txt'])('detect dependency %p', filename => {
124+
// GIVEN
125+
const sourcedir = FileSystem.mkdtemp('source-');
126+
const stagedir = FileSystem.mkdtemp('stage-');
127+
fs.writeFileSync(path.join(sourcedir, filename), 'dummy!');
128+
129+
// WHEN
130+
const found = stageDependencies(sourcedir, stagedir);
131+
132+
// THEN
133+
expect(found).toBeTruthy();
134+
expect(fs.existsSync(path.join(stagedir, filename))).toBeTruthy();
163135
});
164136

165137
test('No known dependencies', () => {
166-
existsSyncMock.mockImplementation(() => false);
167-
expect(hasDependencies('/asset-input')).toEqual(false);
138+
const sourcedir = FileSystem.mkdtemp('source-');
139+
expect(stageDependencies(sourcedir, '/dummy')).toEqual(false);
168140
});
169141
});

packages/@aws-cdk/aws-lambda-python/test/function.test.ts

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
import '@aws-cdk/assert/jest';
2-
import { Runtime } from '@aws-cdk/aws-lambda';
3-
import { Stack } from '@aws-cdk/core';
2+
import { Code, Runtime } from '@aws-cdk/aws-lambda';
3+
import { AssetHashType, AssetOptions, Stack } from '@aws-cdk/core';
44
import { PythonFunction } from '../lib';
55
import { bundle } from '../lib/bundling';
66

77
jest.mock('../lib/bundling', () => {
88
return {
9-
bundle: jest.fn().mockReturnValue({
10-
bind: () => {
11-
return { inlineCode: 'code' };
12-
},
13-
bindToResource: () => { return; },
9+
bundle: jest.fn().mockImplementation((options: AssetOptions): Code => {
10+
const mockObjectKey = (() => {
11+
const hashType = options.assetHashType ?? (options.assetHash ? 'custom' : 'source');
12+
switch (hashType) {
13+
case 'source': return 'SOURCE_MOCK';
14+
case 'output': return 'OUTPUT_MOCK';
15+
case 'custom': {
16+
if (!options.assetHash) { throw new Error('no custom hash'); }
17+
return options.assetHash;
18+
}
19+
}
20+
21+
throw new Error('unexpected asset hash type');
22+
})();
23+
24+
return {
25+
isInline: false,
26+
bind: () => ({
27+
s3Location: {
28+
bucketName: 'mock-bucket-name',
29+
objectKey: mockObjectKey,
30+
},
31+
}),
32+
bindToResource: () => { return; },
33+
};
1434
}),
1535
hasDependencies: jest.fn().mockReturnValue(false),
1636
};
@@ -73,3 +93,53 @@ test('throws with the wrong runtime family', () => {
7393
runtime: Runtime.NODEJS_12_X,
7494
})).toThrow(/Only `PYTHON` runtimes are supported/);
7595
});
96+
97+
test('allows specifying hash type', () => {
98+
new PythonFunction(stack, 'source1', {
99+
entry: 'test/lambda-handler-nodeps',
100+
index: 'index.py',
101+
handler: 'handler',
102+
});
103+
104+
new PythonFunction(stack, 'source2', {
105+
entry: 'test/lambda-handler-nodeps',
106+
index: 'index.py',
107+
handler: 'handler',
108+
assetHashType: AssetHashType.SOURCE,
109+
});
110+
111+
new PythonFunction(stack, 'output', {
112+
entry: 'test/lambda-handler-nodeps',
113+
index: 'index.py',
114+
handler: 'handler',
115+
assetHashType: AssetHashType.OUTPUT,
116+
});
117+
118+
new PythonFunction(stack, 'custom', {
119+
entry: 'test/lambda-handler-nodeps',
120+
index: 'index.py',
121+
handler: 'handler',
122+
assetHash: 'MY_CUSTOM_HASH',
123+
});
124+
125+
expect(stack).toHaveResource('AWS::Lambda::Function', {
126+
Code: {
127+
S3Bucket: 'mock-bucket-name',
128+
S3Key: 'SOURCE_MOCK',
129+
},
130+
});
131+
132+
expect(stack).toHaveResource('AWS::Lambda::Function', {
133+
Code: {
134+
S3Bucket: 'mock-bucket-name',
135+
S3Key: 'OUTPUT_MOCK',
136+
},
137+
});
138+
139+
expect(stack).toHaveResource('AWS::Lambda::Function', {
140+
Code: {
141+
S3Bucket: 'mock-bucket-name',
142+
S3Key: 'MY_CUSTOM_HASH',
143+
},
144+
});
145+
});

0 commit comments

Comments
 (0)