Skip to content

Commit ecb3807

Browse files
antonbabenkoMassimo Maino
and
Massimo Maino
authored
feat: Add support to build automatically npm dependencies (#293)
Co-authored-by: Massimo Maino <[email protected]>
1 parent 1327243 commit ecb3807

File tree

6 files changed

+196
-6
lines changed

6 files changed

+196
-6
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,11 @@ source_path = [
403403
"!vendor/colorful-.+.dist-info/.*",
404404
"!vendor/colorful/__pycache__/?.*",
405405
]
406+
}, {
407+
path = "src/nodejs14.x-app1",
408+
npm_requirements = true,
409+
npm_tmp_dir = "/tmp/dir/location"
410+
prefix_in_zip = "foo/bar1",
406411
}, {
407412
path = "src/python3.8-app3",
408413
commands = [
@@ -424,8 +429,9 @@ source_path = [
424429
]
425430
```
426431

427-
Few notes:
432+
*Few notes:*
428433

434+
- If you specify a source path as a string that references a folder and the runtime begins with `python` or `nodejs`, the build process will automatically build python and nodejs dependencies if `requirements.txt` or `package.json` file will be found in the source folder. If you want to customize this behavior, please use the object notation as explained below.
429435
- All arguments except `path` are optional.
430436
- `patterns` - List of Python regex filenames should satisfy. Default value is "include everything" which is equal to `patterns = [".*"]`. This can also be specified as multiline heredoc string (no comments allowed). Some examples of valid patterns:
431437

@@ -442,10 +448,12 @@ Few notes:
442448
!abc/def/hgk/.* # Filter out again in abc/def/hgk sub folder
443449
```
444450

445-
- `commands` - List of commands to run. If specified, this argument overrides `pip_requirements`.
451+
- `commands` - List of commands to run. If specified, this argument overrides `pip_requirements` and `npm_requirements`.
446452
- `:zip [source] [destination]` is a special command which creates content of current working directory (first argument) and places it inside of path (second argument).
447453
- `pip_requirements` - Controls whether to execute `pip install`. Set to `false` to disable this feature, `true` to run `pip install` with `requirements.txt` found in `path`. Or set to another filename which you want to use instead.
448454
- `pip_tmp_dir` - Set the base directory to make the temporary directory for pip installs. Can be useful for Docker in Docker builds.
455+
- `npm_requirements` - Controls whether to execute `npm install`. Set to `false` to disable this feature, `true` to run `npm install` with `package.json` found in `path`. Or set to another filename which you want to use instead.
456+
- `npm_tmp_dir` - Set the base directory to make the temporary directory for npm installs. Can be useful for Docker in Docker builds.
449457
- `prefix_in_zip` - If specified, will be used as a prefix inside zip-archive. By default, everything installs into the root of zip-archive.
450458

451459
### Building in Docker
@@ -455,7 +463,7 @@ If your Lambda Function or Layer uses some dependencies you can build them in Do
455463
build_in_docker = true
456464
docker_file = "src/python3.8-app1/docker/Dockerfile"
457465
docker_build_root = "src/python3.8-app1/docker"
458-
docker_image = "lambci/lambda:build-python3.8"
466+
docker_image = "public.ecr.aws/sam/build-python3.8"
459467
runtime = "python3.8" # Setting runtime is required when building package in Docker and Lambda Layer resource.
460468

461469
Using this module you can install dependencies from private hosts. To do this, you need for forward SSH agent:

examples/build-package/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ Note that this example may create resources which cost money. Run `terraform des
3838
| <a name="module_lambda_layer_pip_requirements"></a> [lambda\_layer\_pip\_requirements](#module\_lambda\_layer\_pip\_requirements) | ../.. | n/a |
3939
| <a name="module_package_dir"></a> [package\_dir](#module\_package\_dir) | ../../ | n/a |
4040
| <a name="module_package_dir_pip_dir"></a> [package\_dir\_pip\_dir](#module\_package\_dir\_pip\_dir) | ../../ | n/a |
41+
| <a name="module_package_dir_with_npm_install"></a> [package\_dir\_with\_npm\_install](#module\_package\_dir\_with\_npm\_install) | ../../ | n/a |
42+
| <a name="module_package_dir_without_npm_install"></a> [package\_dir\_without\_npm\_install](#module\_package\_dir\_without\_npm\_install) | ../../ | n/a |
4143
| <a name="module_package_dir_without_pip_install"></a> [package\_dir\_without\_pip\_install](#module\_package\_dir\_without\_pip\_install) | ../../ | n/a |
4244
| <a name="module_package_file"></a> [package\_file](#module\_package\_file) | ../../ | n/a |
4345
| <a name="module_package_file_with_pip_requirements"></a> [package\_file\_with\_pip\_requirements](#module\_package\_file\_with\_pip\_requirements) | ../../ | n/a |
4446
| <a name="module_package_with_commands_and_patterns"></a> [package\_with\_commands\_and\_patterns](#module\_package\_with\_commands\_and\_patterns) | ../../ | n/a |
4547
| <a name="module_package_with_docker"></a> [package\_with\_docker](#module\_package\_with\_docker) | ../../ | n/a |
48+
| <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 |
4649
| <a name="module_package_with_patterns"></a> [package\_with\_patterns](#module\_package\_with\_patterns) | ../../ | n/a |
4750
| <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 |
4851

examples/build-package/main.tf

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,45 @@ module "package_with_docker" {
220220
docker_with_ssh_agent = true
221221
# docker_file = "${path.module}/../fixtures/python3.8-app1/docker/Dockerfile"
222222
docker_build_root = "${path.module}/../../docker"
223-
docker_image = "lambci/lambda:build-python3.8"
223+
docker_image = "public.ecr.aws/sam/build-python3.8"
224+
}
225+
226+
# Create zip-archive of a single directory where "npm install" will also be executed (default for nodejs runtime)
227+
module "package_dir_with_npm_install" {
228+
source = "../../"
229+
230+
create_function = false
231+
232+
runtime = "nodejs14.x"
233+
source_path = "${path.module}/../fixtures/nodejs14.x-app1"
234+
}
235+
236+
# Create zip-archive of a single directory without running "npm install" (which is the default for nodejs runtime)
237+
module "package_dir_without_npm_install" {
238+
source = "../../"
239+
240+
create_function = false
241+
242+
runtime = "nodejs14.x"
243+
source_path = [
244+
{
245+
path = "${path.module}/../fixtures/nodejs14.x-app1"
246+
npm_requirements = false
247+
# npm_requirements = true # Will run "npm install" with package.json
248+
}
249+
]
250+
}
251+
252+
# Create zip-archive of a single directory where "npm install" will also be executed using docker
253+
module "package_with_npm_requirements_in_docker" {
254+
source = "../../"
255+
256+
create_function = false
257+
258+
runtime = "nodejs14.x"
259+
source_path = "${path.module}/../fixtures/nodejs14.x-app1"
260+
build_in_docker = true
261+
hash_extra = "something-unique-to-not-conflict-with-module.package_dir_with_npm_install"
224262
}
225263

226264
################################
@@ -240,6 +278,7 @@ module "lambda_layer" {
240278

241279
build_in_docker = true
242280
runtime = "python3.8"
281+
docker_image = "public.ecr.aws/sam/build-python3.8"
243282
docker_file = "${path.module}/../fixtures/python3.8-app1/docker/Dockerfile"
244283
}
245284

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+
};
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.3.0"
7+
}
8+
}

package.py

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,18 @@ def pip_requirements_step(path, prefix=None, required=False, tmp_dir=None):
660660
step('pip', runtime, requirements, prefix, tmp_dir)
661661
hash(requirements)
662662

663+
def npm_requirements_step(path, prefix=None, required=False, tmp_dir=None):
664+
requirements = path
665+
if os.path.isdir(path):
666+
requirements = os.path.join(path, 'package.json')
667+
if not os.path.isfile(requirements):
668+
if required:
669+
raise RuntimeError(
670+
'File not found: {}'.format(requirements))
671+
else:
672+
step('npm', runtime, requirements, prefix, tmp_dir)
673+
hash(requirements)
674+
663675
def commands_step(path, commands):
664676
if not commands:
665677
return
@@ -717,6 +729,9 @@ def commands_step(path, commands):
717729
if runtime.startswith('python'):
718730
pip_requirements_step(
719731
os.path.join(path, 'requirements.txt'))
732+
elif runtime.startswith('nodejs'):
733+
npm_requirements_step(
734+
os.path.join(path, 'package.json'))
720735
step('zip', path, None)
721736
hash(path)
722737

@@ -731,6 +746,7 @@ def commands_step(path, commands):
731746
else:
732747
prefix = claim.get('prefix_in_zip')
733748
pip_requirements = claim.get('pip_requirements')
749+
npm_requirements = claim.get('npm_package_json')
734750
runtime = claim.get('runtime', query.runtime)
735751

736752
if pip_requirements and runtime.startswith('python'):
@@ -740,6 +756,13 @@ def commands_step(path, commands):
740756
pip_requirements_step(pip_requirements, prefix,
741757
required=True, tmp_dir=claim.get('pip_tmp_dir'))
742758

759+
if npm_requirements and runtime.startswith('nodejs'):
760+
if isinstance(npm_requirements, bool) and path:
761+
npm_requirements_step(path, prefix, required=True, tmp_dir=claim.get('npm_tmp_dir'))
762+
else:
763+
npm_requirements_step(npm_requirements, prefix,
764+
required=True, tmp_dir=claim.get('npm_tmp_dir'))
765+
743766
if path:
744767
step('zip', path, prefix)
745768
if patterns:
@@ -793,6 +816,16 @@ def execute(self, build_plan, zip_stream, query):
793816
else:
794817
# XXX: timestamp=0 - what actually do with it?
795818
zs.write_dirs(rd, prefix=prefix, timestamp=0)
819+
elif cmd == 'npm':
820+
runtime, npm_requirements, prefix, tmp_dir = action[1:]
821+
with install_npm_requirements(query, npm_requirements, tmp_dir) as rd:
822+
if rd:
823+
if pf:
824+
self._zip_write_with_filter(zs, pf, rd, prefix,
825+
timestamp=0)
826+
else:
827+
# XXX: timestamp=0 - what actually do with it?
828+
zs.write_dirs(rd, prefix=prefix, timestamp=0)
796829
elif cmd == 'sh':
797830
r, w = os.pipe()
798831
side_ch = os.fdopen(r)
@@ -934,6 +967,89 @@ def install_pip_requirements(query, requirements_file, tmp_dir):
934967
yield temp_dir
935968

936969

970+
@contextmanager
971+
def install_npm_requirements(query, requirements_file, tmp_dir):
972+
# TODO:
973+
# 1. Emit files instead of temp_dir
974+
975+
if not os.path.exists(requirements_file):
976+
yield
977+
return
978+
979+
runtime = query.runtime
980+
artifacts_dir = query.artifacts_dir
981+
temp_dir = query.temp_dir
982+
docker = query.docker
983+
docker_image_tag_id = None
984+
985+
if docker:
986+
docker_file = docker.docker_file
987+
docker_image = docker.docker_image
988+
docker_build_root = docker.docker_build_root
989+
990+
if docker_image:
991+
ok = False
992+
while True:
993+
output = check_output(docker_image_id_command(docker_image))
994+
if output:
995+
docker_image_tag_id = output.decode().strip()
996+
log.debug("DOCKER TAG ID: %s -> %s",
997+
docker_image, docker_image_tag_id)
998+
ok = True
999+
if ok:
1000+
break
1001+
docker_cmd = docker_build_command(
1002+
build_root=docker_build_root,
1003+
docker_file=docker_file,
1004+
tag=docker_image,
1005+
)
1006+
check_call(docker_cmd)
1007+
ok = True
1008+
elif docker_file or docker_build_root:
1009+
raise ValueError('docker_image must be specified '
1010+
'for a custom image future references')
1011+
1012+
log.info('Installing npm requirements: %s', requirements_file)
1013+
with tempdir(tmp_dir) as temp_dir:
1014+
requirements_filename = os.path.basename(requirements_file)
1015+
target_file = os.path.join(temp_dir, requirements_filename)
1016+
shutil.copyfile(requirements_file, target_file)
1017+
1018+
subproc_env = None
1019+
if not docker and OSX:
1020+
subproc_env = os.environ.copy()
1021+
1022+
# Install dependencies into the temporary directory.
1023+
with cd(temp_dir):
1024+
npm_command = ['npm', 'install']
1025+
if docker:
1026+
with_ssh_agent = docker.with_ssh_agent
1027+
chown_mask = '{}:{}'.format(os.getuid(), os.getgid())
1028+
shell_command = [shlex_join(npm_command), '&&',
1029+
shlex_join(['chown', '-R',
1030+
chown_mask, '.'])]
1031+
shell_command = [' '.join(shell_command)]
1032+
check_call(docker_run_command(
1033+
'.', shell_command, runtime,
1034+
image=docker_image_tag_id,
1035+
shell=True, ssh_agent=with_ssh_agent
1036+
))
1037+
else:
1038+
cmd_log.info(shlex_join(npm_command))
1039+
log_handler and log_handler.flush()
1040+
try:
1041+
check_call(npm_command, env=subproc_env)
1042+
except FileNotFoundError as e:
1043+
raise RuntimeError(
1044+
"Nodejs interpreter version equal "
1045+
"to defined lambda runtime ({}) should be "
1046+
"available in system PATH".format(runtime)
1047+
) from e
1048+
1049+
os.remove(target_file)
1050+
yield temp_dir
1051+
1052+
9371053
def docker_image_id_command(tag):
9381054
""""""
9391055
docker_cmd = ['docker', 'images', '--format={{.ID}}', tag]
@@ -1011,7 +1127,7 @@ def docker_run_command(build_root, command, runtime,
10111127
])
10121128

10131129
if not image:
1014-
image = 'lambci/lambda:build-{}'.format(runtime)
1130+
image = 'public.ecr.aws/sam/build-{}'.format(runtime)
10151131

10161132
docker_cmd.append(image)
10171133

@@ -1128,7 +1244,7 @@ def prepare_command(args):
11281244
def build_command(args):
11291245
"""
11301246
Builds a zip file from the source_dir or source_file.
1131-
Installs dependencies with pip automatically.
1247+
Installs dependencies with pip or npm automatically.
11321248
"""
11331249

11341250
log = logging.getLogger('build')

0 commit comments

Comments
 (0)