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(core): local bundling provider #9564

Merged
merged 17 commits into from
Aug 11, 2020
16 changes: 9 additions & 7 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,15 @@ export class Bundling {
return lambda.Code.fromAsset(projectRoot, {
assetHashType: cdk.AssetHashType.BUNDLE,
bundling: {
image,
command: ['bash', '-c', chain([parcelCommand, depsCommand])],
environment: options.parcelEnvironment,
volumes: options.cacheDir
? [{ containerPath: '/parcel-cache', hostPath: options.cacheDir }]
: [],
workingDirectory: path.dirname(containerEntryPath).replace(/\\/g, '/'), // Always use POSIX paths in the container
eladb marked this conversation as resolved.
Show resolved Hide resolved
docker: {
image,
command: ['bash', '-c', chain([parcelCommand, depsCommand])],
environment: options.parcelEnvironment,
volumes: options.cacheDir
? [{ containerPath: '/parcel-cache', hostPath: options.cacheDir }]
: [],
workingDirectory: path.dirname(containerEntryPath).replace(/\\/g, '/'), // Always use POSIX paths in the container
},
},
});
}
Expand Down
46 changes: 27 additions & 19 deletions packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ test('Parcel bundling', () => {
expect(Code.fromAsset).toHaveBeenCalledWith('/project', {
assetHashType: AssetHashType.BUNDLE,
bundling: expect.objectContaining({
environment: {
KEY: 'value',
},
volumes: [{ containerPath: '/parcel-cache', hostPath: '/cache-dir' }],
workingDirectory: '/asset-input/folder',
command: [
'bash', '-c',
'$(node -p "require.resolve(\'parcel\')") build /asset-input/folder/entry.ts --target cdk-lambda --dist-dir /asset-output --no-autoinstall --no-scope-hoist --cache-dir /parcel-cache && mv /asset-output/entry.js /asset-output/index.js',
],
docker: expect.objectContaining({
environment: {
KEY: 'value',
},
volumes: [{ containerPath: '/parcel-cache', hostPath: '/cache-dir' }],
workingDirectory: '/asset-input/folder',
command: [
'bash', '-c',
'$(node -p "require.resolve(\'parcel\')") build /asset-input/folder/entry.ts --target cdk-lambda --dist-dir /asset-output --no-autoinstall --no-scope-hoist --cache-dir /parcel-cache && mv /asset-output/entry.js /asset-output/index.js',
],
}),
}),
});

Expand Down Expand Up @@ -84,9 +86,11 @@ test('Parcel with Windows paths', () => {

expect(Code.fromAsset).toHaveBeenCalledWith('C:\\my-project', expect.objectContaining({
bundling: expect.objectContaining({
command: expect.arrayContaining([
expect.stringContaining('/lib/entry.ts'),
]),
docker: expect.objectContaining({
command: expect.arrayContaining([
expect.stringContaining('/lib/entry.ts'),
]),
}),
}),
}));
});
Expand All @@ -104,10 +108,12 @@ test('Parcel bundling with externals and dependencies', () => {
expect(Code.fromAsset).toHaveBeenCalledWith('/project', {
assetHashType: AssetHashType.BUNDLE,
bundling: expect.objectContaining({
command: [
'bash', '-c',
'$(node -p "require.resolve(\'parcel\')") build /asset-input/folder/entry.ts --target cdk-lambda --dist-dir /asset-output --no-autoinstall --no-scope-hoist && mv /asset-output/entry.js /asset-output/index.js && mv /asset-input/.package.json /asset-output/package.json && cd /asset-output && npm install',
],
docker: expect.objectContaining({
command: [
'bash', '-c',
'$(node -p "require.resolve(\'parcel\')") build /asset-input/folder/entry.ts --target cdk-lambda --dist-dir /asset-output --no-autoinstall --no-scope-hoist && mv /asset-output/entry.js /asset-output/index.js && mv /asset-input/.package.json /asset-output/package.json && cd /asset-output && npm install',
],
}),
}),
});

Expand Down Expand Up @@ -152,9 +158,11 @@ test('Detects yarn.lock', () => {
expect(Code.fromAsset).toHaveBeenCalledWith('/project', {
assetHashType: AssetHashType.BUNDLE,
bundling: expect.objectContaining({
command: expect.arrayContaining([
expect.stringMatching(/yarn\.lock.+yarn install/),
]),
docker: expect.objectContaining({
command: expect.arrayContaining([
expect.stringMatching(/yarn\.lock.+yarn install/),
]),
}),
}),
});
});
Expand Down
6 changes: 4 additions & 2 deletions packages/@aws-cdk/aws-lambda-python/lib/bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export function bundle(options: BundlingOptions): lambda.AssetCode {

return lambda.Code.fromAsset(options.entry, {
bundling: {
image: options.runtime.bundlingDockerImage,
command: ['bash', '-c', depsCommand],
docker: {
image: options.runtime.bundlingDockerImage,
command: ['bash', '-c', depsCommand],
},
},
});
}
Expand Down
30 changes: 18 additions & 12 deletions packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ test('Bundling', () => {
// Correctly bundles
expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'cp -au . /asset-output',
],
docker: expect.objectContaining({
command: [
'bash', '-c',
'cp -au . /asset-output',
],
}),
}),
});

Expand All @@ -46,10 +48,12 @@ test('Bundling with requirements.txt installed', () => {
// Correctly bundles with requirements.txt pip installed
expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'pip3 install -r requirements.txt -t /asset-output && cp -au . /asset-output',
],
docker: expect.objectContaining({
command: [
'bash', '-c',
'pip3 install -r requirements.txt -t /asset-output && cp -au . /asset-output',
],
}),
}),
});
});
Expand All @@ -70,10 +74,12 @@ test('Bundling Python 2.7 with requirements.txt installed', () => {
// Correctly bundles with requirements.txt pip installed
expect(Code.fromAsset).toHaveBeenCalledWith('/project/folder', {
bundling: expect.objectContaining({
command: [
'bash', '-c',
'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output',
],
docker: expect.objectContaining({
command: [
'bash', '-c',
'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output',
],
}),
}),
});
});
30 changes: 17 additions & 13 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,13 +358,15 @@ Example with Python:
new lambda.Function(this, 'Function', {
code: lambda.Code.fromAsset(path.join(__dirname, 'my-python-handler'), {
bundling: {
image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage,
command: [
'bash', '-c', `
pip install -r requirements.txt -t /asset-output &&
cp -au . /asset-output
`,
],
docker: {
image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage,
command: [
'bash', '-c', `
pip install -r requirements.txt -t /asset-output &&
cp -au . /asset-output
`,
],
},
},
}),
runtime: lambda.Runtime.PYTHON_3_6,
Expand All @@ -382,12 +384,14 @@ import * as cdk from '@aws-cdk/core';
new lambda.Function(this, 'Function', {
code: lambda.Code.fromAsset('/path/to/handler', {
bundling: {
image: cdk.BundlingDockerImage.fromAsset('/path/to/dir/with/DockerFile', {
buildArgs: {
ARG1: 'value1',
},
}),
command: ['my', 'cool', 'command'],
docker: {
image: cdk.BundlingDockerImage.fromAsset('/path/to/dir/with/DockerFile', {
buildArgs: {
ARG1: 'value1',
},
}),
command: ['my', 'cool', 'command'],
},
},
}),
// ...
Expand Down
18 changes: 10 additions & 8 deletions packages/@aws-cdk/aws-lambda/test/integ.bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ class TestStack extends Stack {
const fn = new lambda.Function(this, 'Function', {
code: lambda.Code.fromAsset(assetPath, {
bundling: {
image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage,
command: [
'bash', '-c', [
'cp -au . /asset-output',
'cd /asset-output',
'pip install -r requirements.txt -t .',
].join(' && '),
],
docker: {
image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage,
command: [
'bash', '-c', [
'cp -au . /asset-output',
'cd /asset-output',
'pip install -r requirements.txt -t .',
].join(' && '),
],
},
},
}),
runtime: lambda.Runtime.PYTHON_3_6,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ class TestStack extends Stack {
const asset = new assets.Asset(this, 'BundledAsset', {
path: path.join(__dirname, 'markdown-asset'), // /asset-input and working directory in the container
bundling: {
image: BundlingDockerImage.fromAsset(path.join(__dirname, 'alpine-markdown')), // Build an image
command: [
'sh', '-c', `
markdown index.md > /asset-output/index.html
`,
],
docker: {
image: BundlingDockerImage.fromAsset(path.join(__dirname, 'alpine-markdown')), // Build an image
command: [
'sh', '-c', `
markdown index.md > /asset-output/index.html
`,
],
},
},
});
/// !hide
Expand Down
19 changes: 12 additions & 7 deletions packages/@aws-cdk/core/lib/asset-staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ export class AssetStaging extends Construct {
fs.chmodSync(bundleDir, 0o777);

let user: string;
if (options.user) {
user = options.user;
if (options.docker.user) {
user = options.docker.user;
} else { // Default to current user
const userInfo = os.userInfo();
user = userInfo.uid !== -1 // uid is -1 on Windows
Expand All @@ -175,17 +175,22 @@ export class AssetStaging extends Construct {
hostPath: bundleDir,
containerPath: AssetStaging.BUNDLING_OUTPUT_DIR,
},
...options.volumes ?? [],
...options.docker.volumes ?? [],
];

try {
process.stderr.write(`Bundling asset ${this.construct.path}...\n`);
options.image._run({
command: options.command,

if (options.local?.tryBundle(bundleDir)) {
jogold marked this conversation as resolved.
Show resolved Hide resolved
return bundleDir;
}

options.docker.image._run({
command: options.docker.command,
user,
volumes,
environment: options.environment,
workingDirectory: options.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR,
environment: options.docker.environment,
workingDirectory: options.docker.workingDirectory ?? AssetStaging.BUNDLING_INPUT_DIR,
});
} catch (err) {
throw new Error(`Failed to run bundling Docker image for asset ${this.construct.path}: ${err}`);
Expand Down
36 changes: 36 additions & 0 deletions packages/@aws-cdk/core/lib/bundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ import { spawnSync, SpawnSyncOptions } from 'child_process';
* @experimental
*/
export interface BundlingOptions {
/**
* Docker bundling
*/
jogold marked this conversation as resolved.
Show resolved Hide resolved
readonly docker: DockerBundling;

/**
* Local bundling provider.
*
* The provider implements a method `tryBundle()` which should returns `true` if local bundling was
* performed. If `false` is returned, docker bundling will be done.
*
* @default - bundling will only be performed in a Docker container based on the settings in `docker`.
*/
readonly local?: ILocalBundling;
}

/**
* Docker bundling options
*
* @experimental
*/
export interface DockerBundling {
/**
* The Docker image where the command will run.
*/
Expand Down Expand Up @@ -55,6 +77,20 @@ export interface BundlingOptions {
readonly user?: string;
}

/**
* Local bundling
*
* @experimental
*/
export interface ILocalBundling {
/**
* This method is called before attempting docker bundling to allow the
* bundler to be executed locally. If the local bundler exists, and bundling
* was performed locally, return `true`. Otherwise, return `false`.
*/
tryBundle(bundleDir: string): boolean;
jogold marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* A Docker image used for asset bundling
*/
Expand Down
Loading