From 07825c7a004abe1bcbe05db0ddecf4db679f5630 Mon Sep 17 00:00:00 2001 From: Alvin Savoy Date: Mon, 31 Dec 2018 14:04:22 +1100 Subject: [PATCH 01/11] Add build_script var to customize script used An escape hatch for use cases where the built-in build script is not usable; one example is that the Python dependencies require platform-dependent native extensions. --- archive.tf | 5 +++-- hash.py | 8 +++++--- variables.tf | 8 ++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/archive.tf b/archive.tf index 9f5be61..27e3c8a 100644 --- a/archive.tf +++ b/archive.tf @@ -4,8 +4,9 @@ data "external" "archive" { program = ["python", "${path.module}/hash.py"] query = { - runtime = "${var.runtime}" - source_path = "${var.source_path}" + runtime = "${var.runtime}" + source_path = "${var.source_path}" + build_script = "${coalesce(var.build_script, "${path.module}/build.py")}" } } diff --git a/hash.py b/hash.py index 1f9bf44..eae32bd 100644 --- a/hash.py +++ b/hash.py @@ -117,22 +117,24 @@ def update_hash(hash_obj, file_root, file_path): query = { 'runtime': 'python3.6', 'source_path': os.path.join(current_dir, 'tests', 'python3-pip', 'lambda'), + 'build_script': os.path.join(current_dir, 'build.py'), } else: query = json.load(sys.stdin) runtime = query['runtime'] source_path = query['source_path'] +build_script = query['build_script'] # Validate the query. if not source_path: abort('source_path must be set.') # Generate a hash based on file names and content. Also use the -# runtime value and content of build.py because they can have an +# runtime value and content of the build script because they can have an # effect on the resulting archive. content_hash = generate_content_hash(source_path) content_hash.update(runtime.encode()) -with open(os.path.join(current_dir, 'build.py'), 'rb') as build_script_file: +with open(build_script, 'rb') as build_script_file: content_hash.update(build_script_file.read()) # Generate a unique filename based on the hash. @@ -143,7 +145,7 @@ def update_hash(hash_obj, file_root, file_path): # Determine the command to run if Terraform wants to build a new archive. build_command = "python {build_script} {build_data}".format( - build_script=os.path.join(current_dir, 'build.py'), + build_script=build_script, build_data=bytes.decode(base64.b64encode(str.encode( json.dumps({ 'filename': filename, diff --git a/variables.tf b/variables.tf index 8124e5a..756c618 100644 --- a/variables.tf +++ b/variables.tf @@ -8,6 +8,14 @@ variable "handler" { type = "string" } +variable "build_script" { + description = "The path to the script which will compile a zip of the Lambda function" + type = "string" + # Default value is actually "${path.module}/build.py" but is not allowed to + # be specified here + default = "" +} + variable "memory_size" { description = "Amount of memory in MB your Lambda function can use at runtime" type = "string" From 5c955c063c427af9ed70f09cf2395c873acba0cd Mon Sep 17 00:00:00 2001 From: Alvin Savoy Date: Fri, 4 Jan 2019 18:59:00 +1100 Subject: [PATCH 02/11] Add tests/custom-build-script --- tests/custom-build-script/lambda/build.sh | 60 +++++++++++++++++++ tests/custom-build-script/lambda/main.py | 6 ++ .../lambda/requirements.txt | 3 + tests/custom-build-script/main.tf | 21 +++++++ 4 files changed, 90 insertions(+) create mode 100755 tests/custom-build-script/lambda/build.sh create mode 100644 tests/custom-build-script/lambda/main.py create mode 100644 tests/custom-build-script/lambda/requirements.txt create mode 100644 tests/custom-build-script/main.tf diff --git a/tests/custom-build-script/lambda/build.sh b/tests/custom-build-script/lambda/build.sh new file mode 100755 index 0000000..d6d5f14 --- /dev/null +++ b/tests/custom-build-script/lambda/build.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# +# Compiles a Python package into a zip deployable on AWS Lambda. +# +# - Builds Python dependencies into the package, using Docker +# using a Docker image to correctly build native extensions +# - Able to be used with the terraform-aws-lambda module +# +# Dependencies: +# +# - Docker +# - base64 +# - jq +# +# Usage: +# +# $ ./build.sh +# +# - Defaults are to build the current path to out.zip +# +# $ ./build.sh "$(echo { \"filename\": \"out.zip\", \"runtime\": \"python3.7\", \"source_path\": \".\" \} | jq . | base64)" +# +# - Arguments to be provided as JSON, then Base64 encoded + +set -e + +# Extract variables from the JSON-formatted, Base64 encoded input argument. +# This is to conform to arguments as passed in by hash.py +eval "$(echo $1 | base64 -D | jq -r '@sh "FILENAME=\(.filename) RUNTIME=\(.runtime) SOURCE_PATH=\(.source_path)"')" + +# Apply default values for missing arguments +FILENAME="${FILENAME:-out.zip}" +RUNTIME="${RUNTIME:-python3.7}" +SOURCE_PATH="${SOURCE_PATH:-.}" + +# Convert to absolute paths +FILEPATH="$(cd "$(dirname "$FILENAME")"; pwd)/$(basename "$FILENAME")" +SOURCE_PATH="$(cd "$SOURCE_PATH"; pwd)" + +# Setup a temporary path for the build +TEMP_BUILD_PATH=$(mktemp -d "/tmp/build-lambda.XXXXXXXXX") +trap "{ rm -rf '$TEMP_BUILD_PATH'; }" EXIT + +# Copy source code +cp "$SOURCE_PATH"/*.py "$SOURCE_PATH/requirements.txt" "$TEMP_BUILD_PATH"/ + +# Install dependencies, using a Docker image to correctly build native extensions +docker run --rm -t -v "$TEMP_BUILD_PATH:/code" -w /code lambci/lambda:build-$RUNTIME \ + sh -c "pip install -r requirements.txt -t ." + +# Required by AWS Lambda +chmod -R 755 "$TEMP_BUILD_PATH" + +# Cleanup old build output +[ -f "$FILEPATH" ] && rm "$FILEPATH" + +# Build zip package with files at root +(cd "$TEMP_BUILD_PATH"; zip -r $FILEPATH *) + +echo "Created $FILEPATH from $SOURCE_PATH" diff --git a/tests/custom-build-script/lambda/main.py b/tests/custom-build-script/lambda/main.py new file mode 100644 index 0000000..de9967e --- /dev/null +++ b/tests/custom-build-script/lambda/main.py @@ -0,0 +1,6 @@ +def lambda_handler(event, context): + print('importing numpy package') + import numpy as np + print('checking numpy works correctly') + assert np.array_equal(np.array([1, 2]) + 3, np.array([4, 5])) + return 'test passed' diff --git a/tests/custom-build-script/lambda/requirements.txt b/tests/custom-build-script/lambda/requirements.txt new file mode 100644 index 0000000..d9c3d91 --- /dev/null +++ b/tests/custom-build-script/lambda/requirements.txt @@ -0,0 +1,3 @@ +# numpy has native extensions, needs a custom build script to +# install correctly if your host OS differs to Lambda OS +numpy diff --git a/tests/custom-build-script/main.tf b/tests/custom-build-script/main.tf new file mode 100644 index 0000000..e91c230 --- /dev/null +++ b/tests/custom-build-script/main.tf @@ -0,0 +1,21 @@ +terraform { + backend "local" { + path = "terraform.tfstate" + } +} + +provider "aws" { + region = "eu-west-1" +} + +module "lambda" { + source = "../../" + + function_name = "terraform-aws-lambda-test-custom-build-script" + description = "Test custom build script functionality in terraform-aws-lambda" + handler = "main.lambda_handler" + runtime = "python3.7" + + source_path = "${path.module}/lambda" + build_script = "${path.module}/lambda/build.sh" +} From 7e8540fe2be933433ed43043f7e2b840d943aefa Mon Sep 17 00:00:00 2001 From: Raymond Butcher Date: Fri, 4 Jan 2019 11:35:03 +0000 Subject: [PATCH 03/11] Rename build script test to match variable name --- tests/{custom-build-script => build-script}/lambda/build.sh | 0 tests/{custom-build-script => build-script}/lambda/main.py | 0 .../lambda/requirements.txt | 0 tests/{custom-build-script => build-script}/main.tf | 4 ++-- 4 files changed, 2 insertions(+), 2 deletions(-) rename tests/{custom-build-script => build-script}/lambda/build.sh (100%) rename tests/{custom-build-script => build-script}/lambda/main.py (100%) rename tests/{custom-build-script => build-script}/lambda/requirements.txt (100%) rename tests/{custom-build-script => build-script}/main.tf (77%) diff --git a/tests/custom-build-script/lambda/build.sh b/tests/build-script/lambda/build.sh similarity index 100% rename from tests/custom-build-script/lambda/build.sh rename to tests/build-script/lambda/build.sh diff --git a/tests/custom-build-script/lambda/main.py b/tests/build-script/lambda/main.py similarity index 100% rename from tests/custom-build-script/lambda/main.py rename to tests/build-script/lambda/main.py diff --git a/tests/custom-build-script/lambda/requirements.txt b/tests/build-script/lambda/requirements.txt similarity index 100% rename from tests/custom-build-script/lambda/requirements.txt rename to tests/build-script/lambda/requirements.txt diff --git a/tests/custom-build-script/main.tf b/tests/build-script/main.tf similarity index 77% rename from tests/custom-build-script/main.tf rename to tests/build-script/main.tf index e91c230..5311adf 100644 --- a/tests/custom-build-script/main.tf +++ b/tests/build-script/main.tf @@ -11,11 +11,11 @@ provider "aws" { module "lambda" { source = "../../" - function_name = "terraform-aws-lambda-test-custom-build-script" + function_name = "terraform-aws-lambda-test-build-script" description = "Test custom build script functionality in terraform-aws-lambda" handler = "main.lambda_handler" runtime = "python3.7" - source_path = "${path.module}/lambda" + source_path = "${path.module}/lambda" build_script = "${path.module}/lambda/build.sh" } From fdf354c112022be3f640f48bdd150b206b3b79a2 Mon Sep 17 00:00:00 2001 From: Raymond Butcher Date: Fri, 4 Jan 2019 11:35:55 +0000 Subject: [PATCH 04/11] Improve variable description and move next to source path --- variables.tf | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/variables.tf b/variables.tf index 756c618..6ddb9d0 100644 --- a/variables.tf +++ b/variables.tf @@ -8,14 +8,6 @@ variable "handler" { type = "string" } -variable "build_script" { - description = "The path to the script which will compile a zip of the Lambda function" - type = "string" - # Default value is actually "${path.module}/build.py" but is not allowed to - # be specified here - default = "" -} - variable "memory_size" { description = "Amount of memory in MB your Lambda function can use at runtime" type = "string" @@ -44,6 +36,12 @@ variable "source_path" { type = "string" } +variable "build_script" { + description = "The path to a custom build script for creating the Lambda package zip file (defaults to the included build.py)" + type = "string" + default = "" +} + variable "description" { description = "Description of what your Lambda function does" type = "string" From b0e18f45aa47c3f6b60a5e8950f947c52f0a0906 Mon Sep 17 00:00:00 2001 From: Raymond Butcher Date: Fri, 4 Jan 2019 11:38:09 +0000 Subject: [PATCH 05/11] Fix base64 argument for Ubuntu 16.04 It supports `base64 -d` or `base64 --decode` but not `base64 -D`. Not sure where `-D` is supported but I'm hoping `--decode` works there. --- tests/build-script/lambda/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/build-script/lambda/build.sh b/tests/build-script/lambda/build.sh index d6d5f14..271610a 100755 --- a/tests/build-script/lambda/build.sh +++ b/tests/build-script/lambda/build.sh @@ -26,7 +26,7 @@ set -e # Extract variables from the JSON-formatted, Base64 encoded input argument. # This is to conform to arguments as passed in by hash.py -eval "$(echo $1 | base64 -D | jq -r '@sh "FILENAME=\(.filename) RUNTIME=\(.runtime) SOURCE_PATH=\(.source_path)"')" +eval "$(echo $1 | base64 --decode | jq -r '@sh "FILENAME=\(.filename) RUNTIME=\(.runtime) SOURCE_PATH=\(.source_path)"')" # Apply default values for missing arguments FILENAME="${FILENAME:-out.zip}" From 4a31cf68db8dbe599b96d2eee832ca3dcc15d01c Mon Sep 17 00:00:00 2001 From: Raymond Butcher Date: Fri, 4 Jan 2019 15:57:53 +0000 Subject: [PATCH 06/11] Fix Docker permission issues When I tried this, it was creating the Lambda build files as the root user and then it failed trying to clean them up. This now does everything including setting ownership inside Docker. I also removed the default values because I'd rather it break then default to something that may not be intended. --- tests/build-script/lambda/build.sh | 42 +++++++------------ tests/build-script/lambda/{ => src}/main.py | 0 .../lambda/{ => src}/requirements.txt | 0 tests/build-script/main.tf | 2 +- 4 files changed, 15 insertions(+), 29 deletions(-) rename tests/build-script/lambda/{ => src}/main.py (100%) rename tests/build-script/lambda/{ => src}/requirements.txt (100%) diff --git a/tests/build-script/lambda/build.sh b/tests/build-script/lambda/build.sh index 271610a..14ed421 100755 --- a/tests/build-script/lambda/build.sh +++ b/tests/build-script/lambda/build.sh @@ -22,39 +22,25 @@ # # - Arguments to be provided as JSON, then Base64 encoded -set -e +set -euo pipefail # Extract variables from the JSON-formatted, Base64 encoded input argument. # This is to conform to arguments as passed in by hash.py eval "$(echo $1 | base64 --decode | jq -r '@sh "FILENAME=\(.filename) RUNTIME=\(.runtime) SOURCE_PATH=\(.source_path)"')" -# Apply default values for missing arguments -FILENAME="${FILENAME:-out.zip}" -RUNTIME="${RUNTIME:-python3.7}" -SOURCE_PATH="${SOURCE_PATH:-.}" - # Convert to absolute paths -FILEPATH="$(cd "$(dirname "$FILENAME")"; pwd)/$(basename "$FILENAME")" -SOURCE_PATH="$(cd "$SOURCE_PATH"; pwd)" - -# Setup a temporary path for the build -TEMP_BUILD_PATH=$(mktemp -d "/tmp/build-lambda.XXXXXXXXX") -trap "{ rm -rf '$TEMP_BUILD_PATH'; }" EXIT - -# Copy source code -cp "$SOURCE_PATH"/*.py "$SOURCE_PATH/requirements.txt" "$TEMP_BUILD_PATH"/ +SOURCE_DIR=$(cd "$SOURCE_PATH" && pwd) +ZIP_DIR=$(cd "$(dirname "$FILENAME")" && pwd) +ZIP_NAME=$(basename "$FILENAME") # Install dependencies, using a Docker image to correctly build native extensions -docker run --rm -t -v "$TEMP_BUILD_PATH:/code" -w /code lambci/lambda:build-$RUNTIME \ - sh -c "pip install -r requirements.txt -t ." - -# Required by AWS Lambda -chmod -R 755 "$TEMP_BUILD_PATH" - -# Cleanup old build output -[ -f "$FILEPATH" ] && rm "$FILEPATH" - -# Build zip package with files at root -(cd "$TEMP_BUILD_PATH"; zip -r $FILEPATH *) - -echo "Created $FILEPATH from $SOURCE_PATH" +docker run --rm -t -v "$SOURCE_DIR:/src" -v "$ZIP_DIR:/out" lambci/lambda:build-$RUNTIME sh -c " + cp -r /src /build && + cd /build && + pip install -r requirements.txt -t . && + chmod -R 755 . && + zip -r /out/$ZIP_NAME * && + chown \$(stat -c '%u:%g' /out) /out/$ZIP_NAME +" + +echo "Created $FILENAME from $SOURCE_PATH" diff --git a/tests/build-script/lambda/main.py b/tests/build-script/lambda/src/main.py similarity index 100% rename from tests/build-script/lambda/main.py rename to tests/build-script/lambda/src/main.py diff --git a/tests/build-script/lambda/requirements.txt b/tests/build-script/lambda/src/requirements.txt similarity index 100% rename from tests/build-script/lambda/requirements.txt rename to tests/build-script/lambda/src/requirements.txt diff --git a/tests/build-script/main.tf b/tests/build-script/main.tf index 5311adf..7ce5ee8 100644 --- a/tests/build-script/main.tf +++ b/tests/build-script/main.tf @@ -16,6 +16,6 @@ module "lambda" { handler = "main.lambda_handler" runtime = "python3.7" - source_path = "${path.module}/lambda" + source_path = "${path.module}/lambda/src" build_script = "${path.module}/lambda/build.sh" } From b898205bccc5e731c504e71e472bf1786b842943 Mon Sep 17 00:00:00 2001 From: Raymond Butcher Date: Fri, 4 Jan 2019 16:07:59 +0000 Subject: [PATCH 07/11] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f2cf36e..a442c39 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ function name unique per region, for example by setting | attach\_dead\_letter\_config | Set this to true if using the dead_letter_config variable | string | `"false"` | no | | attach\_policy | Set this to true if using the policy variable | string | `"false"` | no | | attach\_vpc\_config | Set this to true if using the vpc_config variable | string | `"false"` | no | +| build\_script | The path to a custom build script for creating the Lambda package zip file (defaults to the included build.py) | string | `""` | no | | dead\_letter\_config | Dead letter configuration for the Lambda function | map | `` | no | | description | Description of what your Lambda function does | string | `"Managed by Terraform"` | no | | enable\_cloudwatch\_logs | Set this to false to disable logging your Lambda output to CloudWatch Logs | string | `"true"` | no | From 76a62c03206bce03127ced1c2dcc3e5fa8bde563 Mon Sep 17 00:00:00 2001 From: Alvin Savoy Date: Sun, 6 Jan 2019 20:20:16 +1100 Subject: [PATCH 08/11] Remove docs for usage with no args --- tests/build-script/lambda/build.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/build-script/lambda/build.sh b/tests/build-script/lambda/build.sh index 14ed421..ddb6631 100755 --- a/tests/build-script/lambda/build.sh +++ b/tests/build-script/lambda/build.sh @@ -13,10 +13,6 @@ # - jq # # Usage: -# -# $ ./build.sh -# -# - Defaults are to build the current path to out.zip # # $ ./build.sh "$(echo { \"filename\": \"out.zip\", \"runtime\": \"python3.7\", \"source_path\": \".\" \} | jq . | base64)" # From d4b5328e4d4a6b08d9bce9fc0b82cbdf3305b590 Mon Sep 17 00:00:00 2001 From: Alvin Savoy Date: Sun, 6 Jan 2019 20:21:47 +1100 Subject: [PATCH 09/11] Fix typo --- tests/build-script/lambda/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/build-script/lambda/build.sh b/tests/build-script/lambda/build.sh index ddb6631..5152065 100755 --- a/tests/build-script/lambda/build.sh +++ b/tests/build-script/lambda/build.sh @@ -2,8 +2,8 @@ # # Compiles a Python package into a zip deployable on AWS Lambda. # -# - Builds Python dependencies into the package, using Docker -# using a Docker image to correctly build native extensions +# - Builds Python dependencies into the package, using a Docker image to +# correctly build native extensions # - Able to be used with the terraform-aws-lambda module # # Dependencies: From 288a9bb2fa31e524be8c2f2cb9b44b9b189d39f5 Mon Sep 17 00:00:00 2001 From: Raymond Butcher Date: Fri, 25 Jan 2019 19:08:37 +0000 Subject: [PATCH 10/11] Change to var.build_command and var.build_paths --- README.md | 3 +- archive.tf | 22 ++-- build.py | 11 +- builds/.gitignore | 1 + built.py | 5 +- hash.py | 111 ++++++++---------- .../lambda/build.sh | 15 +-- .../lambda/src/main.py | 0 .../lambda/src/requirements.txt | 0 tests/build-command/main.tf | 23 ++++ tests/build-script/main.tf | 21 ---- variables.tf | 12 +- 12 files changed, 109 insertions(+), 115 deletions(-) create mode 100644 builds/.gitignore rename tests/{build-script => build-command}/lambda/build.sh (62%) rename tests/{build-script => build-command}/lambda/src/main.py (100%) rename tests/{build-script => build-command}/lambda/src/requirements.txt (100%) create mode 100644 tests/build-command/main.tf delete mode 100644 tests/build-script/main.tf diff --git a/README.md b/README.md index a442c39..313f109 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ function name unique per region, for example by setting | attach\_dead\_letter\_config | Set this to true if using the dead_letter_config variable | string | `"false"` | no | | attach\_policy | Set this to true if using the policy variable | string | `"false"` | no | | attach\_vpc\_config | Set this to true if using the vpc_config variable | string | `"false"` | no | -| build\_script | The path to a custom build script for creating the Lambda package zip file (defaults to the included build.py) | string | `""` | no | +| build\_command | The command that creates the Lambda package zip file | string | `"python build.py '$filename' '$runtime' '$source'"` | no | +| build\_paths | The files or directories used by the build command, to trigger new Lambda package builds whenever build scripts change | list | `` | no | | dead\_letter\_config | Dead letter configuration for the Lambda function | map | `` | no | | description | Description of what your Lambda function does | string | `"Managed by Terraform"` | no | | enable\_cloudwatch\_logs | Set this to false to disable logging your Lambda output to CloudWatch Logs | string | `"true"` | no | diff --git a/archive.tf b/archive.tf index 27e3c8a..76596b3 100644 --- a/archive.tf +++ b/archive.tf @@ -1,12 +1,18 @@ +locals { + module_relpath = "${substr(path.module, length(path.cwd) + 1, -1)}" +} + # Generates a filename for the zip archive based on the contents of the files # in source_path. The filename will change when the source code changes. data "external" "archive" { program = ["python", "${path.module}/hash.py"] query = { - runtime = "${var.runtime}" - source_path = "${var.source_path}" - build_script = "${coalesce(var.build_script, "${path.module}/build.py")}" + build_command = "${var.build_command}" + build_paths = "${jsonencode(var.build_paths)}" + module_relpath = "${local.module_relpath}" + runtime = "${var.runtime}" + source_path = "${var.source_path}" } } @@ -17,7 +23,8 @@ resource "null_resource" "archive" { } provisioner "local-exec" { - command = "${lookup(data.external.archive.result, "build_command")}" + command = "${lookup(data.external.archive.result, "build_command")}" + working_dir = "${path.module}" } } @@ -30,8 +37,9 @@ data "external" "built" { program = ["python", "${path.module}/built.py"] query = { - build_command = "${lookup(data.external.archive.result, "build_command")}" - filename_old = "${lookup(null_resource.archive.triggers, "filename")}" - filename_new = "${lookup(data.external.archive.result, "filename")}" + build_command = "${lookup(data.external.archive.result, "build_command")}" + filename_old = "${lookup(null_resource.archive.triggers, "filename")}" + filename_new = "${lookup(data.external.archive.result, "filename")}" + module_relpath = "${local.module_relpath}" } } diff --git a/build.py b/build.py index faf8730..33304d8 100644 --- a/build.py +++ b/build.py @@ -1,8 +1,6 @@ # Builds a zip file from the source_dir or source_file. # Installs dependencies with pip automatically. -import base64 -import json import os import shutil import subprocess @@ -105,11 +103,10 @@ def create_zip_file(source_dir, target_file): root_dir=source_dir, ) -json_payload = bytes.decode(base64.b64decode(sys.argv[1])) -query = json.loads(json_payload) -filename = query['filename'] -runtime = query['runtime'] -source_path = query['source_path'] + +filename = sys.argv[1] +runtime = sys.argv[2] +source_path = sys.argv[3] absolute_filename = os.path.abspath(filename) diff --git a/builds/.gitignore b/builds/.gitignore new file mode 100644 index 0000000..c4c4ffc --- /dev/null +++ b/builds/.gitignore @@ -0,0 +1 @@ +*.zip diff --git a/built.py b/built.py index 6f96061..3902f9f 100644 --- a/built.py +++ b/built.py @@ -12,6 +12,7 @@ build_command = query['build_command'] filename_old = query['filename_old'] filename_new = query['filename_new'] +module_relpath = query['module_relpath'] # If the old filename (from the Terraform state) matches the new filename # (from hash.py) then the source code has not changed and thus the zip file @@ -29,10 +30,10 @@ # console) then it is possible that Terraform will try to upload # the missing file. I don't know how to tell if Terraform is going # to try to upload the file or not, so always ensure the file exists. - subprocess.check_output(build_command, shell=True) + subprocess.check_output(build_command, shell=True, cwd=module_relpath) # Output the filename to Terraform. json.dump({ - 'filename': filename_new, + 'filename': module_relpath + '/' + filename_new, }, sys.stdout, indent=2) sys.stdout.write('\n') diff --git a/hash.py b/hash.py index eae32bd..7fc73ef 100644 --- a/hash.py +++ b/hash.py @@ -3,20 +3,14 @@ # # Outputs a filename and a command to run if the archive needs to be built. -import base64 import datetime import errno import hashlib import json import os -import re import sys -FILENAME_PREFIX = 'terraform-aws-lambda-' -FILENAME_PATTERN = re.compile(r'^' + FILENAME_PREFIX + r'[0-9a-f]{64}\.zip$') - - def abort(message): """ Exits with an error message. @@ -36,24 +30,21 @@ def delete_old_archives(): now = datetime.datetime.now() delete_older_than = now - datetime.timedelta(days=7) - top = '.terraform' - if os.path.isdir(top): - for name in os.listdir(top): - if FILENAME_PATTERN.match(name): - path = os.path.join(top, name) - try: - file_modified = datetime.datetime.fromtimestamp( - os.path.getmtime(path) - ) - if file_modified < delete_older_than: - os.remove(path) - except OSError as error: - if error.errno == errno.ENOENT: - # Ignore "not found" errors as they are probably race - # conditions between multiple usages of this module. - pass - else: - raise + for name in os.listdir('builds'): + if name.endswith('.zip'): + try: + file_modified = datetime.datetime.fromtimestamp( + os.path.getmtime(name) + ) + if file_modified < delete_older_than: + os.remove(name) + except OSError as error: + if error.errno == errno.ENOENT: + # Ignore "not found" errors as they are probably race + # conditions between multiple usages of this module. + pass + else: + raise def list_files(top_path): @@ -72,22 +63,23 @@ def list_files(top_path): return results -def generate_content_hash(source_path): +def generate_content_hash(source_paths): """ - Generate a content hash of the source path. + Generate a content hash of the source paths. """ sha256 = hashlib.sha256() - if os.path.isdir(source_path): - source_dir = source_path - for source_file in list_files(source_dir): + for source_path in source_paths: + if os.path.isdir(source_path): + source_dir = source_path + for source_file in list_files(source_dir): + update_hash(sha256, source_dir, source_file) + else: + source_dir = os.path.dirname(source_path) + source_file = source_path update_hash(sha256, source_dir, source_file) - else: - source_dir = os.path.dirname(source_path) - source_file = source_path - update_hash(sha256, source_dir, source_file) return sha256 @@ -109,53 +101,42 @@ def update_hash(hash_obj, file_root, file_path): hash_obj.update(data) - -current_dir = os.path.dirname(__file__) - # Parse the query. -if len(sys.argv) > 1 and sys.argv[1] == '--test': - query = { - 'runtime': 'python3.6', - 'source_path': os.path.join(current_dir, 'tests', 'python3-pip', 'lambda'), - 'build_script': os.path.join(current_dir, 'build.py'), - } -else: - query = json.load(sys.stdin) +query = json.load(sys.stdin) +build_command = query['build_command'] +build_paths = json.loads(query['build_paths']) +module_relpath = query['module_relpath'] runtime = query['runtime'] source_path = query['source_path'] -build_script = query['build_script'] # Validate the query. if not source_path: abort('source_path must be set.') +# Change working directory to the module path +# so references to build.py will work. +os.chdir(module_relpath) + # Generate a hash based on file names and content. Also use the -# runtime value and content of the build script because they can have an -# effect on the resulting archive. -content_hash = generate_content_hash(source_path) +# runtime value, build command, and content of the build paths +# because they can have an effect on the resulting archive. +content_hash = generate_content_hash([source_path] + build_paths) content_hash.update(runtime.encode()) -with open(build_script, 'rb') as build_script_file: - content_hash.update(build_script_file.read()) +content_hash.update(build_command.encode()) # Generate a unique filename based on the hash. -filename = '.terraform/{prefix}{content_hash}.zip'.format( - prefix=FILENAME_PREFIX, +filename = 'builds/{content_hash}.zip'.format( content_hash=content_hash.hexdigest(), ) -# Determine the command to run if Terraform wants to build a new archive. -build_command = "python {build_script} {build_data}".format( - build_script=build_script, - build_data=bytes.decode(base64.b64encode(str.encode( - json.dumps({ - 'filename': filename, - 'source_path': source_path, - 'runtime': runtime, - }) - ) - ), - ) -) +# Replace variables in the build command with calculated values. +replacements = { + '$filename': filename, + '$runtime': runtime, + '$source': source_path, +} +for old, new in replacements.items(): + build_command = build_command.replace(old, new) # Delete previous archives. delete_old_archives() diff --git a/tests/build-script/lambda/build.sh b/tests/build-command/lambda/build.sh similarity index 62% rename from tests/build-script/lambda/build.sh rename to tests/build-command/lambda/build.sh index 5152065..b9e90b0 100755 --- a/tests/build-script/lambda/build.sh +++ b/tests/build-command/lambda/build.sh @@ -9,20 +9,17 @@ # Dependencies: # # - Docker -# - base64 -# - jq # # Usage: # -# $ ./build.sh "$(echo { \"filename\": \"out.zip\", \"runtime\": \"python3.7\", \"source_path\": \".\" \} | jq . | base64)" -# -# - Arguments to be provided as JSON, then Base64 encoded +# $ ./build.sh set -euo pipefail -# Extract variables from the JSON-formatted, Base64 encoded input argument. -# This is to conform to arguments as passed in by hash.py -eval "$(echo $1 | base64 --decode | jq -r '@sh "FILENAME=\(.filename) RUNTIME=\(.runtime) SOURCE_PATH=\(.source_path)"')" +# Read variables from command line arguments +FILENAME=$1 +RUNTIME=$2 +SOURCE_PATH=$3 # Convert to absolute paths SOURCE_DIR=$(cd "$SOURCE_PATH" && pwd) @@ -33,7 +30,7 @@ ZIP_NAME=$(basename "$FILENAME") docker run --rm -t -v "$SOURCE_DIR:/src" -v "$ZIP_DIR:/out" lambci/lambda:build-$RUNTIME sh -c " cp -r /src /build && cd /build && - pip install -r requirements.txt -t . && + pip install --progress-bar off -r requirements.txt -t . && chmod -R 755 . && zip -r /out/$ZIP_NAME * && chown \$(stat -c '%u:%g' /out) /out/$ZIP_NAME diff --git a/tests/build-script/lambda/src/main.py b/tests/build-command/lambda/src/main.py similarity index 100% rename from tests/build-script/lambda/src/main.py rename to tests/build-command/lambda/src/main.py diff --git a/tests/build-script/lambda/src/requirements.txt b/tests/build-command/lambda/src/requirements.txt similarity index 100% rename from tests/build-script/lambda/src/requirements.txt rename to tests/build-command/lambda/src/requirements.txt diff --git a/tests/build-command/main.tf b/tests/build-command/main.tf new file mode 100644 index 0000000..382e067 --- /dev/null +++ b/tests/build-command/main.tf @@ -0,0 +1,23 @@ +terraform { + backend "local" { + path = "terraform.tfstate" + } +} + +provider "aws" { + region = "eu-west-1" +} + +module "lambda" { + source = "../../" + + function_name = "terraform-aws-lambda-test-build-command" + description = "Test custom build command functionality in terraform-aws-lambda" + handler = "main.lambda_handler" + runtime = "python3.7" + + source_path = "${path.module}/lambda/src" + + build_command = "${path.module}/lambda/build.sh '$filename' '$runtime' '$source'" + build_paths = ["${path.module}/lambda/build.sh"] +} diff --git a/tests/build-script/main.tf b/tests/build-script/main.tf deleted file mode 100644 index 7ce5ee8..0000000 --- a/tests/build-script/main.tf +++ /dev/null @@ -1,21 +0,0 @@ -terraform { - backend "local" { - path = "terraform.tfstate" - } -} - -provider "aws" { - region = "eu-west-1" -} - -module "lambda" { - source = "../../" - - function_name = "terraform-aws-lambda-test-build-script" - description = "Test custom build script functionality in terraform-aws-lambda" - handler = "main.lambda_handler" - runtime = "python3.7" - - source_path = "${path.module}/lambda/src" - build_script = "${path.module}/lambda/build.sh" -} diff --git a/variables.tf b/variables.tf index 6ddb9d0..2a64535 100644 --- a/variables.tf +++ b/variables.tf @@ -36,10 +36,16 @@ variable "source_path" { type = "string" } -variable "build_script" { - description = "The path to a custom build script for creating the Lambda package zip file (defaults to the included build.py)" +variable "build_command" { + description = "The command that creates the Lambda package zip file" type = "string" - default = "" + default = "python build.py '$filename' '$runtime' '$source'" +} + +variable "build_paths" { + description = "The files or directories used by the build command, to trigger new Lambda package builds whenever build scripts change" + type = "list" + default = ["build.py"] } variable "description" { From 9835cdd8b11674e66dfeba8319d78e2f2fc838bf Mon Sep 17 00:00:00 2001 From: Raymond Butcher Date: Fri, 1 Feb 2019 12:35:56 +0000 Subject: [PATCH 11/11] Use asdf for terraform testing --- tests/.tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/.tool-versions diff --git a/tests/.tool-versions b/tests/.tool-versions new file mode 100644 index 0000000..a0c26f2 --- /dev/null +++ b/tests/.tool-versions @@ -0,0 +1 @@ +terraform 0.11.11