-
Notifications
You must be signed in to change notification settings - Fork 148
[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 14 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,85 @@ | ||
""" | ||
Action to resolve NodeJS dependencies using NPM | ||
""" | ||
|
||
import logging | ||
from aws_lambda_builders.actions import BaseAction, Purpose, ActionFailedError | ||
from .utils import OSUtils | ||
from .npm import SubprocessNpm, NpmExecutionError | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class NodejsNpmPackAction(BaseAction): | ||
|
||
NAME = 'NpmPack' | ||
DESCRIPTION = "Packaging source using NPM" | ||
PURPOSE = Purpose.COMPILE_SOURCE | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def __init__(self, artifacts_dir, scratch_dir, manifest_path, runtime, osutils=None, subprocess_npm=None): | ||
gojko marked this conversation as resolved.
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
|
||
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.run(['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) | ||
|
||
LOG.debug("NODEJS extracting to %s", self.artifacts_dir) | ||
|
||
self.osutils.extract_tarfile(tarfile_path, self.artifacts_dir) | ||
|
||
except NpmExecutionError as ex: | ||
raise ActionFailedError(str(ex)) | ||
|
||
|
||
class NodejsNpmInstallAction(BaseAction): | ||
|
||
NAME = 'NpmInstall' | ||
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.
Show resolved
Hide resolved
|
||
|
||
def execute(self): | ||
try: | ||
LOG.debug("NODEJS installing in: %s from: %s", self.artifacts_dir, self.manifest_path) | ||
|
||
self.subprocess_npm.run( | ||
['install', '-q', '--no-audit', '--no-save', '--production'], | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cwd=self.artifacts_dir | ||
) | ||
|
||
except NpmExecutionError as ex: | ||
raise ActionFailedError(str(ex)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
""" | ||
Wrapper around calling npm through a subprocess. | ||
""" | ||
|
||
import logging | ||
|
||
from aws_lambda_builders.exceptions import LambdaBuilderError | ||
|
||
from .utils import OSUtils | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class NpmExecutionError(LambdaBuilderError): | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
MESSAGE = "NPM Failed: {message}" | ||
|
||
|
||
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' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the future if there are multiple paths of npm on your system, how would you imagine passing them in? or maybe thats not a big concern. I started a PR here: #35 that would help with that. But I'm going to take a step back and maybe write a mini design doc to get started on it. I was curious to know if you had any comments on that? |
||
|
||
self.npm_exe = npm_exe | ||
|
||
def run(self, args, cwd=None): | ||
|
||
if not isinstance(args, list): | ||
raise ValueError('args must be a list') | ||
|
||
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(message=err.decode('utf8').strip()) | ||
|
||
return out.decode('utf8').strip() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
""" | ||
Commonly used utilities | ||
""" | ||
|
||
import os | ||
import shutil | ||
import tarfile | ||
import subprocess | ||
|
||
|
||
class OSUtils(object): | ||
jfuss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def environ(self): | ||
return os.environ | ||
|
||
def file_exists(self, filename): | ||
return os.path.isfile(filename) | ||
|
||
def extract_tarfile(self, tarfile_path, unpack_dir): | ||
with tarfile.open(tarfile_path, 'r:*') as tar: | ||
tar.extractall(unpack_dir) | ||
|
||
def get_directory_contents(self, path): | ||
return os.listdir(path) | ||
|
||
def joinpath(self, *args): | ||
return os.path.join(*args) | ||
|
||
def copytree(self, source, destination): | ||
if not os.path.exists(destination): | ||
self.makedirs(destination) | ||
names = self.get_directory_contents(source) | ||
for name in names: | ||
new_source = os.path.join(source, name) | ||
new_destination = os.path.join(destination, name) | ||
if os.path.isdir(new_source): | ||
self.copytree(new_source, new_destination) | ||
else: | ||
shutil.copy2(new_source, new_destination) | ||
|
||
def popen(self, command, stdout=None, stderr=None, env=None, cwd=None): | ||
p = subprocess.Popen(command, stdout=stdout, stderr=stderr, env=env, cwd=cwd) | ||
return p | ||
|
||
@property | ||
def pipe(self): | ||
return subprocess.PIPE | ||
|
||
def dirname(self, path): | ||
return os.path.dirname(path) | ||
|
||
def abspath(self, path): | ||
return os.path.abspath(path) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
""" | ||
NodeJS NPM Workflow | ||
""" | ||
|
||
import hashlib | ||
|
||
from aws_lambda_builders.workflow import BaseWorkflow, Capability | ||
from aws_lambda_builders.actions import CopySourceAction | ||
|
||
from .actions import NodejsNpmPackAction, NodejsNpmInstallAction | ||
from .utils import OSUtils | ||
|
||
|
||
def unique_dir(manifest_path): | ||
sanathkr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
md5 = hashlib.md5() | ||
md5.update(manifest_path.encode('utf8')) | ||
return md5.hexdigest() | ||
|
||
|
||
class NodejsNpmWorkflow(BaseWorkflow): | ||
|
||
NAME = "NodejsNpmBuilder" | ||
|
||
CAPABILITY = Capability(language="nodejs", | ||
dependency_manager="npm", | ||
application_framework=None) | ||
|
||
EXCLUDED_FILES = (".aws-sam") | ||
|
||
def __init__(self, | ||
source_dir, | ||
artifacts_dir, | ||
scratch_dir, | ||
manifest_path, | ||
runtime=None, | ||
osutils=None, | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
**kwargs): | ||
|
||
super(NodejsNpmWorkflow, self).__init__(source_dir, | ||
artifacts_dir, | ||
scratch_dir, | ||
manifest_path, | ||
runtime=runtime, | ||
**kwargs) | ||
|
||
if osutils is None: | ||
osutils = OSUtils() | ||
|
||
tar_dest_dir = osutils.joinpath(scratch_dir, unique_dir(manifest_path)) | ||
tar_package_dir = osutils.joinpath(tar_dest_dir, 'package') | ||
|
||
self.actions = [ | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
NodejsNpmPackAction(tar_dest_dir, scratch_dir, manifest_path, runtime), | ||
CopySourceAction(tar_package_dir, artifacts_dir, excludes=self.EXCLUDED_FILES), | ||
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,44 @@ | ||
import io | ||
|
||
import pytest | ||
|
||
import sys | ||
|
||
import os | ||
|
||
from aws_lambda_builders.workflows.nodejs_npm import utils | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
@pytest.fixture | ||
def osutils(): | ||
return utils.OSUtils() | ||
|
||
|
||
class TestOSUtils(object): | ||
gojko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def test_dirname_returns_directory_for_path(self, tmpdir, osutils): | ||
dirname = osutils.dirname(sys.executable) | ||
|
||
assert dirname == os.path.dirname(sys.executable) | ||
|
||
def test_abspath_returns_absolute_path(self, tmpdir, osutils): | ||
|
||
result = osutils.abspath('.') | ||
|
||
assert os.path.isabs(result) | ||
|
||
assert result == os.path.abspath('.') | ||
|
||
def test_popen_can_accept_cwd(self, tmpdir, osutils): | ||
|
||
testdata_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata') | ||
|
||
p = osutils.popen([sys.executable, 'cwd.py'], | ||
stdout=osutils.pipe, | ||
stderr=osutils.pipe, | ||
cwd=testdata_dir) | ||
|
||
out, err = p.communicate() | ||
|
||
assert p.returncode == 0 | ||
|
||
assert out.decode('utf8').strip() == os.path.abspath(testdata_dir) |
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; |
Uh oh!
There was an error while loading. Please reload this page.