Skip to content

Commit aadf043

Browse files
author
Amadeusz Żołnowski
committed
feat: Respect the package-lock.json for a NodeJS Lambda function (#423)
Respect the `package-lock.json` so NodeJS Lambda for reproducible builds which are critical in production environments. Similarly like for the Poetry, copy a lock file, if such is present, to a temporary build directory. npm will use a `package-lock.json` file when available in a working directory. In the example `package.json`, require lower `requests` version to demonstrate `package-lock.json` usage. `package.json` specifies `~0.2.0` and the latest available matching version is `0.2.2`, but `package-lock.json` freezes version `0.2.1` and that version gets installed with this change, while previously the `0.2.2` would be installed.
1 parent cc9be0a commit aadf043

File tree

8 files changed

+204
-7
lines changed

8 files changed

+204
-7
lines changed

examples/build-package/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Note that this example may create resources which cost money. Run `terraform des
4545
| <a name="module_package_dir_poetry"></a> [package\_dir\_poetry](#module\_package\_dir\_poetry) | ../../ | n/a |
4646
| <a name="module_package_dir_poetry_no_docker"></a> [package\_dir\_poetry\_no\_docker](#module\_package\_dir\_poetry\_no\_docker) | ../../ | n/a |
4747
| <a name="module_package_dir_with_npm_install"></a> [package\_dir\_with\_npm\_install](#module\_package\_dir\_with\_npm\_install) | ../../ | n/a |
48+
| <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 |
4849
| <a name="module_package_dir_without_npm_install"></a> [package\_dir\_without\_npm\_install](#module\_package\_dir\_without\_npm\_install) | ../../ | n/a |
4950
| <a name="module_package_dir_without_pip_install"></a> [package\_dir\_without\_pip\_install](#module\_package\_dir\_without\_pip\_install) | ../../ | n/a |
5051
| <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
5354
| <a name="module_package_src_poetry2"></a> [package\_src\_poetry2](#module\_package\_src\_poetry2) | ../../ | n/a |
5455
| <a name="module_package_with_commands_and_patterns"></a> [package\_with\_commands\_and\_patterns](#module\_package\_with\_commands\_and\_patterns) | ../../ | n/a |
5556
| <a name="module_package_with_docker"></a> [package\_with\_docker](#module\_package\_with\_docker) | ../../ | n/a |
57+
| <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 |
5658
| <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 |
5759
| <a name="module_package_with_patterns"></a> [package\_with\_patterns](#module\_package\_with\_patterns) | ../../ | n/a |
5860
| <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 |

examples/build-package/main.tf

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,18 @@ module "package_dir_with_npm_install" {
365365
source_path = "${path.module}/../fixtures/nodejs14.x-app1"
366366
}
367367

368+
# Create zip-archive of a single directory where "npm install" will also be
369+
# executed (default for nodejs runtime). This example has package-lock.json which
370+
# is respected when installing dependencies.
371+
module "package_dir_with_npm_install_lock_file" {
372+
source = "../../"
373+
374+
create_function = false
375+
376+
runtime = "nodejs14.x"
377+
source_path = "${path.module}/../fixtures/nodejs14.x-app2"
378+
}
379+
368380
# Create zip-archive of a single directory without running "npm install" (which is the default for nodejs runtime)
369381
module "package_dir_without_npm_install" {
370382
source = "../../"
@@ -393,6 +405,20 @@ module "package_with_npm_requirements_in_docker" {
393405
hash_extra = "something-unique-to-not-conflict-with-module.package_dir_with_npm_install"
394406
}
395407

408+
# Create zip-archive of a single directory where "npm install" will also be
409+
# executed using docker. This example has package-lock.json which is respected
410+
# when installing dependencies.
411+
module "package_with_npm_lock_in_docker" {
412+
source = "../../"
413+
414+
create_function = false
415+
416+
runtime = "nodejs14.x"
417+
source_path = "${path.module}/../fixtures/nodejs14.x-app2"
418+
build_in_docker = true
419+
hash_extra = "something-unique-to-not-conflict-with-module.package_dir_with_npm_install"
420+
}
421+
396422
################################
397423
# Build package in Docker and
398424
# use it to deploy Lambda Layer
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
module.exports.hello = async (event) => {
4+
console.log(event);
5+
return {
6+
statusCode: 200,
7+
body: JSON.stringify(
8+
{
9+
message: `Go Serverless.tf! Your Nodejs function executed successfully!`,
10+
input: event,
11+
},
12+
null,
13+
2
14+
),
15+
};
16+
};

examples/fixtures/nodejs14.x-app2/package-lock.json

Lines changed: 83 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "nodejs14.x-app1",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"dependencies": {
6+
"requests": "^0.2.0"
7+
}
8+
}

package.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,14 @@ def npm_requirements_step(path, prefix=None, required=False, tmp_dir=None):
733733
requirements = path
734734
if os.path.isdir(path):
735735
requirements = os.path.join(path, "package.json")
736+
npm_lock_file = os.path.join(path, "package-lock.json")
737+
else:
738+
npm_lock_file = os.path.join(os.path.dirname(path), "package-lock.json")
739+
740+
if os.path.isfile(npm_lock_file):
741+
hash(npm_lock_file)
742+
log.info("Added npm lock file: %s", npm_lock_file)
743+
736744
if not os.path.isfile(requirements):
737745
if required:
738746
raise RuntimeError("File not found: {}".format(requirements))
@@ -1395,9 +1403,10 @@ def install_npm_requirements(query, requirements_file, tmp_dir):
13951403

13961404
log.info("Installing npm requirements: %s", requirements_file)
13971405
with tempdir(tmp_dir) as temp_dir:
1398-
requirements_filename = os.path.basename(requirements_file)
1399-
target_file = os.path.join(temp_dir, requirements_filename)
1400-
shutil.copyfile(requirements_file, target_file)
1406+
temp_copy = TemporaryCopy(os.path.dirname(requirements_file), temp_dir, log)
1407+
temp_copy.add(os.path.basename(requirements_file))
1408+
temp_copy.add("package-lock.json", required=False)
1409+
temp_copy.copy_to_target_dir()
14011410

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

1445-
os.remove(target_file)
1454+
temp_copy.remove_from_target_dir()
14461455
yield temp_dir
14471456

14481457

1458+
class TemporaryCopy:
1459+
"""Temporarily copy files to a specified location and remove them when
1460+
not needed.
1461+
"""
1462+
1463+
def __init__(self, source_dir_path, target_dir_path, logger=None):
1464+
"""Initialise with a target and a source directories."""
1465+
self.source_dir_path = source_dir_path
1466+
self.target_dir_path = target_dir_path
1467+
self._filenames = []
1468+
self._logger = logger
1469+
1470+
def _make_source_path(self, filename):
1471+
return os.path.join(self.source_dir_path, filename)
1472+
1473+
def _make_target_path(self, filename):
1474+
return os.path.join(self.target_dir_path, filename)
1475+
1476+
def add(self, filename, *, required=True):
1477+
"""Add a file to be copied from from source to target directory
1478+
when `TemporaryCopy.copy_to_target_dir()` is called.
1479+
1480+
By default, the file must exist in the source directory. Set `required`
1481+
to `False` if the file is optional.
1482+
"""
1483+
if os.path.exists(self._make_source_path(filename)):
1484+
self._filenames.append(filename)
1485+
elif required:
1486+
raise RuntimeError("File not found: {}".format(filename))
1487+
1488+
def copy_to_target_dir(self):
1489+
"""Copy files (added so far) to the target directory."""
1490+
for filename in self._filenames:
1491+
if self._logger:
1492+
self._logger.info("Copying temporarily '%s'", filename)
1493+
1494+
shutil.copyfile(
1495+
self._make_source_path(filename),
1496+
self._make_target_path(filename),
1497+
)
1498+
1499+
def remove_from_target_dir(self):
1500+
"""Remove files (added so far) from the target directory."""
1501+
for filename in self._filenames:
1502+
if self._logger:
1503+
self._logger.info("Removing temporarily copied '%s'", filename)
1504+
1505+
try:
1506+
os.remove(self._make_target_path(filename))
1507+
except FileNotFoundError:
1508+
pass
1509+
1510+
14491511
def docker_image_id_command(tag):
14501512
""""""
14511513
docker_cmd = ["docker", "images", "--format={{.ID}}", tag]

wrappers/docker-build/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ module "wrapper" {
44
for_each = var.items
55

66
build_args = try(each.value.build_args, var.defaults.build_args, {})
7-
build_target = try(each.value.build_target, var.defaults.build_target, null)
87
builder = try(each.value.builder, var.defaults.builder, null)
8+
build_target = try(each.value.build_target, var.defaults.build_target, null)
99
cache_from = try(each.value.cache_from, var.defaults.cache_from, [])
1010
create_ecr_repo = try(each.value.create_ecr_repo, var.defaults.create_ecr_repo, false)
1111
create_sam_metadata = try(each.value.create_sam_metadata, var.defaults.create_sam_metadata, false)

wrappers/main.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ module "wrapper" {
7575
lambda_at_edge_logs_all_regions = try(each.value.lambda_at_edge_logs_all_regions, var.defaults.lambda_at_edge_logs_all_regions, true)
7676
lambda_role = try(each.value.lambda_role, var.defaults.lambda_role, "")
7777
layer_name = try(each.value.layer_name, var.defaults.layer_name, "")
78-
layer_skip_destroy = try(each.value.layer_skip_destroy, var.defaults.layer_skip_destroy, false)
7978
layers = try(each.value.layers, var.defaults.layers, null)
79+
layer_skip_destroy = try(each.value.layer_skip_destroy, var.defaults.layer_skip_destroy, false)
8080
license_info = try(each.value.license_info, var.defaults.license_info, "")
8181
local_existing_package = try(each.value.local_existing_package, var.defaults.local_existing_package, null)
8282
logging_application_log_level = try(each.value.logging_application_log_level, var.defaults.logging_application_log_level, "INFO")
@@ -101,8 +101,8 @@ module "wrapper" {
101101
putin_khuylo = try(each.value.putin_khuylo, var.defaults.putin_khuylo, true)
102102
recreate_missing_package = try(each.value.recreate_missing_package, var.defaults.recreate_missing_package, true)
103103
recursive_loop = try(each.value.recursive_loop, var.defaults.recursive_loop, null)
104-
replace_security_groups_on_destroy = try(each.value.replace_security_groups_on_destroy, var.defaults.replace_security_groups_on_destroy, null)
105104
replacement_security_group_ids = try(each.value.replacement_security_group_ids, var.defaults.replacement_security_group_ids, null)
105+
replace_security_groups_on_destroy = try(each.value.replace_security_groups_on_destroy, var.defaults.replace_security_groups_on_destroy, null)
106106
reserved_concurrent_executions = try(each.value.reserved_concurrent_executions, var.defaults.reserved_concurrent_executions, -1)
107107
role_description = try(each.value.role_description, var.defaults.role_description, null)
108108
role_force_detach_policies = try(each.value.role_force_detach_policies, var.defaults.role_force_detach_policies, true)

0 commit comments

Comments
 (0)