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
Show file tree
Hide file tree
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
Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down
26 changes: 26 additions & 0 deletions examples/build-package/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "../../"
Expand Down Expand Up @@ -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
Expand Down
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
Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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 = {
Expand Down