Skip to content

feat: Respect the package-lock.json for a NodeJS Lambda function #681

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
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
2 changes: 2 additions & 0 deletions examples/build-package/README.md
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ Note that this example may create resources which cost money. Run `terraform des
| <a name="module_package_dir_poetry"></a> [package\_dir\_poetry](#module\_package\_dir\_poetry) | ../../ | n/a |
| <a name="module_package_dir_poetry_no_docker"></a> [package\_dir\_poetry\_no\_docker](#module\_package\_dir\_poetry\_no\_docker) | ../../ | n/a |
| <a name="module_package_dir_with_npm_install"></a> [package\_dir\_with\_npm\_install](#module\_package\_dir\_with\_npm\_install) | ../../ | n/a |
| <a name="module_package_dir_with_npm_install_lock_file"></a> [package\_dir\_with\_npm\_install\_lock\_file](#module\_package\_dir\_with\_npm\_install\_lock\_file) | ../../ | n/a |
| <a name="module_package_dir_without_npm_install"></a> [package\_dir\_without\_npm\_install](#module\_package\_dir\_without\_npm\_install) | ../../ | n/a |
| <a name="module_package_dir_without_pip_install"></a> [package\_dir\_without\_pip\_install](#module\_package\_dir\_without\_pip\_install) | ../../ | n/a |
| <a name="module_package_file"></a> [package\_file](#module\_package\_file) | ../../ | n/a |
@@ -53,6 +54,7 @@ Note that this example may create resources which cost money. Run `terraform des
| <a name="module_package_src_poetry2"></a> [package\_src\_poetry2](#module\_package\_src\_poetry2) | ../../ | n/a |
| <a name="module_package_with_commands_and_patterns"></a> [package\_with\_commands\_and\_patterns](#module\_package\_with\_commands\_and\_patterns) | ../../ | n/a |
| <a name="module_package_with_docker"></a> [package\_with\_docker](#module\_package\_with\_docker) | ../../ | n/a |
| <a name="module_package_with_npm_lock_in_docker"></a> [package\_with\_npm\_lock\_in\_docker](#module\_package\_with\_npm\_lock\_in\_docker) | ../../ | n/a |
| <a name="module_package_with_npm_requirements_in_docker"></a> [package\_with\_npm\_requirements\_in\_docker](#module\_package\_with\_npm\_requirements\_in\_docker) | ../../ | n/a |
| <a name="module_package_with_patterns"></a> [package\_with\_patterns](#module\_package\_with\_patterns) | ../../ | n/a |
| <a name="module_package_with_pip_requirements_in_docker"></a> [package\_with\_pip\_requirements\_in\_docker](#module\_package\_with\_pip\_requirements\_in\_docker) | ../../ | n/a |
26 changes: 26 additions & 0 deletions examples/build-package/main.tf
Original file line number Diff line number Diff line change
@@ -365,6 +365,18 @@ module "package_dir_with_npm_install" {
source_path = "${path.module}/../fixtures/nodejs14.x-app1"
}

# Create zip-archive of a single directory where "npm install" will also be
# executed (default for nodejs runtime). This example has package-lock.json which
# is respected when installing dependencies.
module "package_dir_with_npm_install_lock_file" {
source = "../../"

create_function = false

runtime = "nodejs14.x"
source_path = "${path.module}/../fixtures/nodejs14.x-app2"
}

# Create zip-archive of a single directory without running "npm install" (which is the default for nodejs runtime)
module "package_dir_without_npm_install" {
source = "../../"
@@ -393,6 +405,20 @@ module "package_with_npm_requirements_in_docker" {
hash_extra = "something-unique-to-not-conflict-with-module.package_dir_with_npm_install"
}

# Create zip-archive of a single directory where "npm install" will also be
# executed using docker. This example has package-lock.json which is respected
# when installing dependencies.
module "package_with_npm_lock_in_docker" {
source = "../../"

create_function = false

runtime = "nodejs14.x"
source_path = "${path.module}/../fixtures/nodejs14.x-app2"
build_in_docker = true
hash_extra = "something-unique-to-not-conflict-with-module.package_dir_with_npm_install"
}

################################
# Build package in Docker and
# use it to deploy Lambda Layer
16 changes: 16 additions & 0 deletions examples/fixtures/nodejs14.x-app2/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

module.exports.hello = async (event) => {
console.log(event);
return {
statusCode: 200,
body: JSON.stringify(
{
message: `Go Serverless.tf! Your Nodejs function executed successfully!`,
input: event,
},
null,
2
),
};
};
83 changes: 83 additions & 0 deletions examples/fixtures/nodejs14.x-app2/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions examples/fixtures/nodejs14.x-app2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "nodejs14.x-app1",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"requests": "^0.2.0"
}
}
78 changes: 70 additions & 8 deletions package.py
Original file line number Diff line number Diff line change
@@ -733,6 +733,14 @@ def npm_requirements_step(path, prefix=None, required=False, tmp_dir=None):
requirements = path
if os.path.isdir(path):
requirements = os.path.join(path, "package.json")
npm_lock_file = os.path.join(path, "package-lock.json")
else:
npm_lock_file = os.path.join(os.path.dirname(path), "package-lock.json")

if os.path.isfile(npm_lock_file):
hash(npm_lock_file)
log.info("Added npm lock file: %s", npm_lock_file)

if not os.path.isfile(requirements):
if required:
raise RuntimeError("File not found: {}".format(requirements))
@@ -1088,7 +1096,7 @@ def install_pip_requirements(query, requirements_file, tmp_dir):
ok = True
elif docker_file or docker_build_root:
raise ValueError(
"docker_image must be specified " "for a custom image future references"
"docker_image must be specified for a custom image future references"
)

working_dir = os.getcwd()
@@ -1108,7 +1116,7 @@ def install_pip_requirements(query, requirements_file, tmp_dir):
elif OSX:
# Workaround for OSX when XCode command line tools'
# python becomes the main system python interpreter
os_path = "{}:/Library/Developer/CommandLineTools" "/usr/bin".format(
os_path = "{}:/Library/Developer/CommandLineTools/usr/bin".format(
os.environ["PATH"]
)
subproc_env = os.environ.copy()
@@ -1390,14 +1398,15 @@ def install_npm_requirements(query, requirements_file, tmp_dir):
ok = True
elif docker_file or docker_build_root:
raise ValueError(
"docker_image must be specified " "for a custom image future references"
"docker_image must be specified for a custom image future references"
)

log.info("Installing npm requirements: %s", requirements_file)
with tempdir(tmp_dir) as temp_dir:
requirements_filename = os.path.basename(requirements_file)
target_file = os.path.join(temp_dir, requirements_filename)
shutil.copyfile(requirements_file, target_file)
temp_copy = TemporaryCopy(os.path.dirname(requirements_file), temp_dir, log)
temp_copy.add(os.path.basename(requirements_file))
temp_copy.add("package-lock.json", required=False)
temp_copy.copy_to_target_dir()

subproc_env = None
npm_exec = "npm"
@@ -1442,10 +1451,63 @@ def install_npm_requirements(query, requirements_file, tmp_dir):
"available in system PATH".format(runtime)
) from e

os.remove(target_file)
temp_copy.remove_from_target_dir()
yield temp_dir


class TemporaryCopy:
"""Temporarily copy files to a specified location and remove them when
not needed.
"""

def __init__(self, source_dir_path, target_dir_path, logger=None):
"""Initialise with a target and a source directories."""
self.source_dir_path = source_dir_path
self.target_dir_path = target_dir_path
self._filenames = []
self._logger = logger

def _make_source_path(self, filename):
return os.path.join(self.source_dir_path, filename)

def _make_target_path(self, filename):
return os.path.join(self.target_dir_path, filename)

def add(self, filename, *, required=True):
"""Add a file to be copied from from source to target directory
when `TemporaryCopy.copy_to_target_dir()` is called.
By default, the file must exist in the source directory. Set `required`
to `False` if the file is optional.
"""
if os.path.exists(self._make_source_path(filename)):
self._filenames.append(filename)
elif required:
raise RuntimeError("File not found: {}".format(filename))

def copy_to_target_dir(self):
"""Copy files (added so far) to the target directory."""
for filename in self._filenames:
if self._logger:
self._logger.info("Copying temporarily '%s'", filename)

shutil.copyfile(
self._make_source_path(filename),
self._make_target_path(filename),
)

def remove_from_target_dir(self):
"""Remove files (added so far) from the target directory."""
for filename in self._filenames:
if self._logger:
self._logger.info("Removing temporarily copied '%s'", filename)

try:
os.remove(self._make_target_path(filename))
except FileNotFoundError:
pass


def docker_image_id_command(tag):
""""""
docker_cmd = ["docker", "images", "--format={{.ID}}", tag]
@@ -1649,7 +1711,7 @@ def prepare_command(args):
timestamp = timestamp_now_ns()
was_missing = True
else:
timestamp = "<WARNING: Missing lambda zip artifacts " "wouldn't be restored>"
timestamp = "<WARNING: Missing lambda zip artifacts wouldn't be restored>"

# Replace variables in the build command with calculated values.
build_data = {