-
Notifications
You must be signed in to change notification settings - Fork 154
[WIP] support for building nodejs_npm functions #44
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
Changes from 10 commits
140fd87
03bddad
50b55cf
bab2c3d
de32212
ccf9e6d
3ac1a38
3810cd8
a08723a
9ac2e18
cec44ae
4a00c1e
3e945b2
d91c537
2542cdb
d183932
1008117
dc98d9f
79fc787
77e6116
d141caf
7d5a1d0
70517bf
7eaad47
bbb92a5
9270a81
6f23b11
53f9d34
2c5d780
ed23979
fcd47a9
09d4190
607079e
6863067
fdc2707
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ | |
| """ | ||
|
|
||
| import aws_lambda_builders.workflows.python_pip | ||
| import aws_lambda_builders.workflows.nodejs_npm | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """ | ||
| Builds NodeJS Lambda functions using NPM dependency manager | ||
| """ | ||
|
|
||
| from .workflow import NodejsNpmWorkflow |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| """ | ||
| Action to resolve NodeJS dependencies using NPM | ||
| """ | ||
|
|
||
| import logging | ||
| from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError | ||
| from aws_lambda_builders.workflows.python_pip.utils import OSUtils | ||
| from .npm import SubprocessNpm, NpmError | ||
|
|
||
| LOG = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class NodejsNpmPackAction(BaseAction): | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| NAME = 'CopySource' | ||
| DESCRIPTION = "Packaging source using NPM" | ||
| PURPOSE = Purpose.COPY_SOURCE | ||
|
|
||
| def __init__(self, artifacts_dir, scratch_dir, manifest_path, runtime, osutils=None, subprocess_npm=None): | ||
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self.artifacts_dir = artifacts_dir | ||
| self.manifest_path = manifest_path | ||
| self.scratch_dir = scratch_dir | ||
| self.runtime = runtime | ||
|
|
||
| self.osutils = osutils | ||
| if osutils is None: | ||
| self.osutils = OSUtils() | ||
|
|
||
| self.subprocess_npm = subprocess_npm | ||
|
|
||
| if self.subprocess_npm is None: | ||
| self.subprocess_npm = SubprocessNpm(self.osutils) | ||
|
|
||
| def execute(self): | ||
| try: | ||
jfuss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if not self.osutils.file_exists(self.manifest_path): | ||
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| raise ActionFailedError('package.json not found in: %s' % self.manifest_path) | ||
|
|
||
| package_path = "file:{}".format(self.osutils.abspath(self.osutils.dirname(self.manifest_path))) | ||
jfuss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| LOG.debug("NODEJS packaging %s to %s", package_path, self.scratch_dir) | ||
|
|
||
| tarfile_name = self.subprocess_npm.main(['pack', '-q', package_path], cwd=self.scratch_dir) | ||
|
||
|
|
||
| LOG.debug("NODEJS packed to %s", tarfile_name) | ||
|
|
||
| tarfile_path = self.osutils.joinpath(self.scratch_dir, tarfile_name) | ||
|
|
||
| self.osutils.extract_tarfile(tarfile_path, tarfile_path + '-unpacked') | ||
|
|
||
| self.osutils.copytree(self.osutils.joinpath(tarfile_path + '-unpacked', 'package'), self.artifacts_dir) | ||
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| except NpmError as ex: | ||
| raise ActionFailedError(str(ex)) | ||
|
|
||
|
|
||
| class NodejsNpmInstallAction(BaseAction): | ||
|
|
||
| NAME = 'ResolveDependencies' | ||
| DESCRIPTION = "Installing dependencies from NPM" | ||
| PURPOSE = Purpose.RESOLVE_DEPENDENCIES | ||
|
|
||
| def __init__(self, artifacts_dir, scratch_dir, manifest_path, runtime, osutils=None, subprocess_npm=None): | ||
| self.artifacts_dir = artifacts_dir | ||
| self.manifest_path = manifest_path | ||
| self.scratch_dir = scratch_dir | ||
| self.runtime = runtime | ||
|
|
||
| self.osutils = osutils | ||
| if osutils is None: | ||
| self.osutils = OSUtils() | ||
|
|
||
| self.subprocess_npm = subprocess_npm | ||
|
|
||
| if self.subprocess_npm is None: | ||
| self.subprocess_npm = SubprocessNpm(self.osutils) | ||
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def execute(self): | ||
| try: | ||
| LOG.debug("NODEJS installing in: %s from: %s", self.artifacts_dir, self.manifest_path) | ||
|
|
||
| if not self.osutils.file_exists(self.osutils.joinpath(self.artifacts_dir, 'package.json')): | ||
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| raise ActionFailedError('package.json not found in: %s' % self.artifacts_dir) | ||
|
|
||
| self.subprocess_npm.main( | ||
| ['install', '-q', '--no-audit', '--no-save', '--production'], | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| cwd=self.artifacts_dir | ||
| ) | ||
|
|
||
| except NpmError as ex: | ||
| raise ActionFailedError(str(ex)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| """ | ||
| Wrapper around calling npm through a subprocess. | ||
| """ | ||
|
|
||
| import logging | ||
|
|
||
| from aws_lambda_builders.workflows.python_pip.utils import OSUtils | ||
|
|
||
| LOG = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class NpmError(Exception): | ||
| pass | ||
|
|
||
|
|
||
| class NpmExecutionError(NpmError): | ||
| def __init__(self, err): | ||
| super(NpmExecutionError, self).__init__( | ||
| 'NPM failed: %s' % err) | ||
|
|
||
|
|
||
| class SubprocessNpm(object): | ||
|
|
||
| def __init__(self, osutils=None, npm_exe=None): | ||
| if osutils is None: | ||
| osutils = OSUtils() | ||
| self.osutils = osutils | ||
|
|
||
| if npm_exe is None: | ||
| npm_exe = 'npm' | ||
|
||
|
|
||
| self.npm_exe = npm_exe | ||
|
|
||
| def main(self, args, cwd=None): | ||
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if not isinstance(args, list): | ||
| raise NpmExecutionError('args must be a list') | ||
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| invoke_npm = [self.npm_exe] + args | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| LOG.debug("executing NPM: %s", invoke_npm) | ||
|
|
||
| p = self.osutils.popen(invoke_npm, | ||
| stdout=self.osutils.pipe, | ||
| stderr=self.osutils.pipe, | ||
| cwd=cwd) | ||
|
|
||
| out, err = p.communicate() | ||
|
|
||
| if p.returncode != 0: | ||
| raise NpmExecutionError(err.decode('utf8').strip()) | ||
|
|
||
| return out.decode('utf8').strip() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """ | ||
| NodeJS NPM Workflow | ||
| """ | ||
|
|
||
| from aws_lambda_builders.workflow import BaseWorkflow, Capability | ||
|
|
||
| from .actions import NodejsNpmPackAction, NodejsNpmInstallAction | ||
|
|
||
|
|
||
| class NodejsNpmWorkflow(BaseWorkflow): | ||
|
|
||
| NAME = "NodejsNpmBuilder" | ||
|
|
||
| CAPABILITY = Capability(language="nodejs", | ||
| dependency_manager="npm", | ||
| application_framework=None) | ||
|
|
||
| def __init__(self, | ||
| source_dir, | ||
| artifacts_dir, | ||
| scratch_dir, | ||
| manifest_path, | ||
| runtime=None, **kwargs): | ||
|
|
||
| super(NodejsNpmWorkflow, self).__init__(source_dir, | ||
| artifacts_dir, | ||
| scratch_dir, | ||
| manifest_path, | ||
| runtime=runtime, | ||
| **kwargs) | ||
|
|
||
| self.actions = [ | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| NodejsNpmPackAction(artifacts_dir, scratch_dir, manifest_path, runtime), | ||
gojko marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| NodejsNpmInstallAction(artifacts_dir, scratch_dir, manifest_path, runtime) | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import os | ||
|
|
||
| print(os.getcwd()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
|
|
||
| import os | ||
| import shutil | ||
| import tempfile | ||
| from unittest import TestCase | ||
|
|
||
| from aws_lambda_builders.builder import LambdaBuilder | ||
| from aws_lambda_builders.exceptions import WorkflowFailedError | ||
|
|
||
|
|
||
| class TestNodejsNpmWorkflow(TestCase): | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| Verifies that `nodejs_npm` workflow works by building a Lambda using NPM | ||
| """ | ||
|
|
||
| TEST_DATA_FOLDER = os.path.join(os.path.dirname(__file__), "testdata") | ||
|
|
||
| def setUp(self): | ||
| self.artifacts_dir = tempfile.mkdtemp() | ||
| self.scratch_dir = tempfile.mkdtemp() | ||
|
|
||
| self.no_deps = os.path.join(self.TEST_DATA_FOLDER, "no-deps") | ||
|
|
||
| self.builder = LambdaBuilder(language="nodejs", | ||
| dependency_manager="npm", | ||
| application_framework=None) | ||
| self.runtime = "nodejs8.10" | ||
|
|
||
| def tearDown(self): | ||
| shutil.rmtree(self.artifacts_dir) | ||
| shutil.rmtree(self.scratch_dir) | ||
|
|
||
| def test_builds_project_without_dependencies(self): | ||
| source_dir = os.path.join(self.TEST_DATA_FOLDER, "no-deps") | ||
|
|
||
| self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, | ||
| os.path.join(source_dir, "package.json"), | ||
| runtime=self.runtime) | ||
|
|
||
| expected_files = {"package.json", "included.js"} | ||
| output_files = set(os.listdir(self.artifacts_dir)) | ||
| self.assertEquals(expected_files, output_files) | ||
|
|
||
| def test_builds_project_with_remote_dependencies(self): | ||
| source_dir = os.path.join(self.TEST_DATA_FOLDER, "npm-deps") | ||
|
|
||
| self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, | ||
| os.path.join(source_dir, "package.json"), | ||
| runtime=self.runtime) | ||
|
|
||
| expected_files = {"package.json", "included.js", "node_modules"} | ||
| output_files = set(os.listdir(self.artifacts_dir)) | ||
| self.assertEquals(expected_files, output_files) | ||
|
|
||
| expected_modules = {"minimal-request-promise"} | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| output_modules = set(os.listdir(os.path.join(self.artifacts_dir, "node_modules"))) | ||
| self.assertEquals(expected_modules, output_modules) | ||
|
|
||
| def test_fails_if_npm_cannot_resolve_dependencies(self): | ||
|
|
||
| source_dir = os.path.join(self.TEST_DATA_FOLDER, "broken-deps") | ||
|
|
||
| with self.assertRaises(WorkflowFailedError) as ctx: | ||
| self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, | ||
| os.path.join(source_dir, "package.json"), | ||
| runtime=self.runtime) | ||
|
|
||
| self.assertIn("No matching version found for [email protected]_EXISTENT", str(ctx.exception)) | ||
|
|
||
| def test_fails_if_package_json_is_broken(self): | ||
|
|
||
| source_dir = os.path.join(self.TEST_DATA_FOLDER, "broken-package") | ||
|
|
||
| with self.assertRaises(WorkflowFailedError) as ctx: | ||
| self.builder.build(source_dir, self.artifacts_dir, self.scratch_dir, | ||
| os.path.join(source_dir, "package.json"), | ||
| runtime=self.runtime) | ||
|
|
||
| self.assertIn("Unexpected end of JSON input", str(ctx.exception)) | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| //excluded | ||
| const x = 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| //included | ||
| const x = 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "name": "brokendeps", | ||
| "version": "1.0.0", | ||
| "description": "", | ||
| "files": ["included.js"], | ||
| "keywords": [], | ||
| "author": "", | ||
| "license": "APACHE2.0", | ||
| "dependencies": { | ||
| "minimal-request-promise": "0.0.0-NON_EXISTENT" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| //excluded | ||
| const x = 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| //included | ||
| const x = 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "broken-package-json", | ||
| "version": "1.0.0", | ||
| "description": "", | ||
| "author": "", | ||
| "license": "APACHE2.0", | ||
| "dependencies": { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| //excluded | ||
| const x = 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| //included | ||
| const x = 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "nodeps", | ||
| "version": "1.0.0", | ||
| "description": "", | ||
| "files": ["included.js"], | ||
| "keywords": [], | ||
| "author": "", | ||
| "license": "APACHE2.0" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| //excluded | ||
| const x = 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| //included | ||
| const x = 1; |
Uh oh!
There was an error while loading. Please reload this page.