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-nodejs): run parcel in a docker container #7842

Merged
merged 5 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions buildspec-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ version: 0.2
phases:
install:
commands:
# Start docker daemon inside the container
- nohup /usr/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2&
- timeout 15 sh -c "until docker info; do echo .; sleep 1; done"

# Install yarn if it wasn't already present in the image
- yarn --version || npm -g install yarn
build:
Expand Down
4 changes: 4 additions & 0 deletions buildspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ version: 0.2
phases:
install:
commands:
# Start docker daemon inside the container
- nohup /usr/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2&
- timeout 15 sh -c "until docker info; do echo .; sleep 1; done"

# Install yarn if it wasn't already present in the image
- yarn --version || npm -g install yarn
pre_build:
Expand Down
9 changes: 1 addition & 8 deletions packages/@aws-cdk/aws-lambda-nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,7 @@

This library provides constructs for Node.js Lambda functions.

To use this module, you will need to add a dependency on `parcel-bundler` in your
`package.json`:

```
yarn add parcel-bundler@^1
# or
npm install parcel-bundler@^1
```
To use this module, you will need to have Docker installed.

### Node.js Function
Define a `NodejsFunction`:
Expand Down
117 changes: 82 additions & 35 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { findPkgPath, updatePkg } from './util';
import { findGitPath, findPkgPath } from './util';

/**
* Builder options
Expand Down Expand Up @@ -40,74 +40,121 @@ export interface BuilderOptions {
/**
* The node version to use as target for Babel
*/
readonly nodeVersion?: string;
readonly nodeVersion: string;

/**
* The docker tag of the node base image to use in the parcel-bundler docker image
*
* @see https://hub.docker.com/_/node/?tab=tags
*/
readonly nodeDockerTag: string;
}

/**
* Builder
*/
export class Builder {
private readonly parcelBinPath: string;
private readonly pkgPath: string;

constructor(private readonly options: BuilderOptions) {
let parcelPkgPath: string;
try {
parcelPkgPath = require.resolve('parcel-bundler/package.json'); // This will throw if `parcel-bundler` cannot be found
} catch (err) {
throw new Error('It looks like parcel-bundler is not installed. Please install v1.x of parcel-bundler with yarn or npm.');
}
const parcelDir = path.dirname(parcelPkgPath);
const parcelPkg = JSON.parse(fs.readFileSync(parcelPkgPath, 'utf8'));
private readonly originalPkg: Buffer;

if (!parcelPkg.version || !/^1\./.test(parcelPkg.version)) { // Peer dependency on parcel v1.x
throw new Error(`This module has a peer dependency on parcel-bundler v1.x. Got v${parcelPkg.version}.`);
}
private readonly originalPkgJson: { [key: string]: any };

this.parcelBinPath = path.join(parcelDir, parcelPkg.bin.parcel);
constructor(private readonly options: BuilderOptions) {
// Original package.json
this.pkgPath = findPkgPath();
this.originalPkg = fs.readFileSync(this.pkgPath);
this.originalPkgJson = JSON.parse(this.originalPkg.toString());
}

/**
* Build with parcel in a Docker container
*/
public build(): void {
const pkgPath = findPkgPath();
let originalPkg;

try {
if (this.options.nodeVersion && pkgPath) {
// Update engines.node (Babel target)
originalPkg = updatePkg(pkgPath, {
engines: { node: `>= ${this.options.nodeVersion}` },
});
this.updatePkg();

const dockerBuildArgs = [
'build',
'--build-arg', `NODE_TAG=${this.options.nodeDockerTag}`,
'-t', 'parcel-bundler',
path.join(__dirname, '../parcel-bundler'),
];

const build = spawnSync('docker', dockerBuildArgs);

if (build.error) {
throw build.error;
}

if (build.status !== 0) {
throw new Error(`[Status ${build.status}] stdout: ${build.stdout?.toString().trim()}\n\n\nstderr: ${build.stderr?.toString().trim()}`);
}

const args = [
'build', this.options.entry,
'--out-dir', this.options.outDir,
// Find the git root and mount it in the container. This allows Parcel to
// find the same modules/dependencies as the ones available "locally". It
// also supports monorepos.
const projectRoot = path.dirname(findGitPath());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eladb this is the fix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies that the project root is the git root which means:

  1. That the project directory must be inside a git repo. This is not always the case. For example, last time I checked, when you used GitHub as a source for CodePipeline, the CodeBuild project did not include a git clone but rather a copy of the source tree (without a .git directory).
  2. Maybe we need to allow people to configure this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have maybe a suggestion to find the root of a monorepo?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we allow people to specify it and default to searching for .git, but if we can't find we omit an error explaining how to manually configure.

const containerProjectRoot = '/project';
const containerOutDir = '/out';
const containerCacheDir = '/cache';
const containerEntryPath = path.join(containerProjectRoot, path.relative(projectRoot, path.resolve(this.options.entry)));

const dockerRunArgs = [
'run', '--rm',
'-v', `${projectRoot}:${containerProjectRoot}`,
'-v', `${path.resolve(this.options.outDir)}:${containerOutDir}`,
...(this.options.cacheDir ? ['-v', `${path.resolve(this.options.cacheDir)}:${containerCacheDir}`] : []),
'-w', path.dirname(containerEntryPath),
'parcel-bundler',
];
const parcelArgs = [
'parcel', 'build', containerEntryPath,
'--out-dir', containerOutDir,
'--out-file', 'index.js',
'--global', this.options.global,
'--target', 'node',
'--bundle-node-modules',
'--log-level', '2',
!this.options.minify && '--no-minify',
!this.options.sourceMaps && '--no-source-maps',
...this.options.cacheDir
? ['--cache-dir', this.options.cacheDir]
: [],
...(this.options.cacheDir ? ['--cache-dir', containerCacheDir] : []),
].filter(Boolean) as string[];

const parcel = spawnSync(this.parcelBinPath, args);
const parcel = spawnSync('docker', [...dockerRunArgs, ...parcelArgs]);

if (parcel.error) {
throw parcel.error;
}

if (parcel.status !== 0) {
throw new Error(parcel.stdout.toString().trim());
throw new Error(`[Status ${parcel.status}] stdout: ${parcel.stdout?.toString().trim()}\n\n\nstderr: ${parcel.stderr?.toString().trim()}`);
}
} catch (err) {
throw new Error(`Failed to build file at ${this.options.entry}: ${err}`);
} finally { // Always restore package.json to original
if (pkgPath && originalPkg) {
fs.writeFileSync(pkgPath, originalPkg);
}
this.restorePkg();
}
}

/**
* Updates the package.json to configure Parcel
*/
private updatePkg() {
const updateData: { [key: string]: any } = {};
// Update engines.node (Babel target)
updateData.engines = { node: `>= ${this.options.nodeVersion}` };

// Write new package.json
if (Object.keys(updateData).length !== 0) {
fs.writeFileSync(this.pkgPath, JSON.stringify({
...this.originalPkgJson,
...updateData,
}, null, 2));
}
}

private restorePkg() {
fs.writeFileSync(this.pkgPath, this.originalPkg);
}
}
14 changes: 12 additions & 2 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions {
* @default - `.cache` in the root directory
*/
readonly cacheDir?: string;

/**
* The docker tag of the node base image to use in the parcel-bundler docker image
*
* @see https://hub.docker.com/_/node/?tab=tags
*
* @default - the `process.versions.node` alpine image
*/
readonly nodeDockerTag?: string;
}

/**
Expand Down Expand Up @@ -94,6 +103,7 @@ export class NodejsFunction extends lambda.Function {
sourceMaps: props.sourceMaps,
cacheDir: props.cacheDir,
nodeVersion: extractVersion(runtime),
nodeDockerTag: props.nodeDockerTag || `${process.versions.node}-alpine`,
});
builder.build();

Expand Down Expand Up @@ -156,11 +166,11 @@ function findDefiningFile(): string {
/**
* Extracts the version from the runtime
*/
function extractVersion(runtime: lambda.Runtime): string | undefined {
function extractVersion(runtime: lambda.Runtime): string {
const match = runtime.name.match(/nodejs(\d+)/);

if (!match) {
return undefined;
throw new Error('Cannot extract version from runtime.');
}

return match[1];
Expand Down
41 changes: 21 additions & 20 deletions packages/@aws-cdk/aws-lambda-nodejs/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';

// From https://github.com/errwischt/stacktrace-parser/blob/master/src/stack-trace-parser.js
const STACK_RE = /^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
Expand Down Expand Up @@ -50,35 +51,35 @@ export function nodeMajorVersion(): number {
}

/**
* Finds closest package.json path
* Finds the closest path containg a path
*/
export function findPkgPath(): string | undefined {
let pkgPath;
function findClosestPathContaining(p: string): string {
let closestPath;

for (const path of module.paths) {
pkgPath = path.replace(/node_modules$/, 'package.json');
if (fs.existsSync(pkgPath)) {
for (const nodeModulesPath of module.paths) {
closestPath = path.join(path.dirname(nodeModulesPath), p);
if (fs.existsSync(closestPath)) {
break;
}
}

return pkgPath;
if (!closestPath) {
throw new Error(`Cannot find path ${p}.`);
}

return closestPath;
}

/**
* Updates the package.json and returns the original
* Finds closest package.json path
*/
export function updatePkg(pkgPath: string, data: any): Buffer {
const original = fs.readFileSync(pkgPath);

const pkgJson = JSON.parse(original.toString());

const updated = {
...pkgJson,
...data,
};

fs.writeFileSync(pkgPath, JSON.stringify(updated, null, 2));
export function findPkgPath(): string {
return findClosestPathContaining('package.json');
}

return original;
/**
* Finds closest .git/ and returns the path containing this directory
*/
export function findGitPath(): string {
return findClosestPathContaining(`.git${path.sep}`);
}
1 change: 0 additions & 1 deletion packages/@aws-cdk/aws-lambda-nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
"cdk-build-tools": "0.0.0",
"cdk-integ-tools": "0.0.0",
"fs-extra": "^8.1.0",
"parcel-bundler": "^1.12.4",
"pkglint": "0.0.0"
},
"dependencies": {
Expand Down
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-lambda-nodejs/parcel-bundler/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# runs the parcel-bundler npm package to package and install dependencies of nodejs lambda functions
ARG NODE_TAG
FROM node:${NODE_TAG}

RUN yarn global add parcel-bundler@^1

# add the global node_modules folder to NODE_PATH so that plugins can find parcel-bundler
ENV NODE_PATH /usr/local/share/.config/yarn/global/node_modules

CMD [ "parcel" ]
Loading