diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md index 0934f2d785..40f0455f33 100644 --- a/DEVELOPMENT_GUIDE.md +++ b/DEVELOPMENT_GUIDE.md @@ -91,6 +91,10 @@ Move back to your SAM CLI directory and re-run init, If necessary: open requirem Running Tests ------------- +### Unit testing with one Python version + +If you're trying to do a quick run, it's ok to use the current python version. Run `make pr`. + ### Unit testing with multiple Python versions [tox](http://tox.readthedocs.io/en/latest/) is used to run tests against diff --git a/README.md b/README.md index 7dacf629fb..b4f254d42c 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Project Status - \[x\] `nodejs4.3` - \[x\] `nodejs6.10` - \[x\] `nodejs8.10` + - \[x\] `nodejs10.x` - \[x\] `java8` - \[x\] `python2.7` - \[x\] `python3.6` @@ -91,6 +92,7 @@ Project Status - \[x\] `nodejs4.3` - \[x\] `nodejs6.10` - \[x\] `nodejs8.10` + - \[x\] `nodejs10.x` - \[x\] `java8` - \[x\] `python2.7` - \[x\] `python3.6` diff --git a/designs/sam_build_cmd.md b/designs/sam_build_cmd.md index b4cfb86ac5..6624977d07 100644 --- a/designs/sam_build_cmd.md +++ b/designs/sam_build_cmd.md @@ -50,7 +50,8 @@ Success criteria for the change - Python with PIP - Golang with Go CLI - Dotnetcore with DotNet CLI -2. Each Lambda function in SAM template gets built +2. Each Lambda function in SAM template gets built by default unless a `function_identifier` (LogicalID) is passed + to the build command 3. Produce stable builds (best effort): If the source files did not change, built artifacts should not change. 4. Built artifacts should \"just work\" with `sam local` and diff --git a/docs/usage.md b/docs/usage.md index 8fb6b5fecd..59548cf95c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -451,7 +451,7 @@ Debugging Golang functions Golang function debugging is slightly different when compared to Node.JS, Java, and Python. We require -[delve](https://github.com/derekparker/delve) as the debugger, and wrap +[delve](https://github.com/go-delve/delve) as the debugger, and wrap your function with it at runtime. The debugger is run in headless mode, listening on the debug port. @@ -463,7 +463,7 @@ You must compile [delve]{.title-ref} to run in the container and provide its local path via the [\--debugger-path]{.title-ref} argument. Build delve locally as follows: -`GOARCH=amd64 GOOS=linux go build -o /dlv github.com/derekparker/delve/cmd/dlv` +`GOARCH=amd64 GOOS=linux go build -o /dlv github.com/go-delve/delve/cmd/dlv` NOTE: The output path needs to end in [/dlv]{.title-ref}. The docker container will expect the dlv binary to be in the \ diff --git a/samcli/__init__.py b/samcli/__init__.py index 60b01efcdd..0fad9dd1de 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = '0.15.0' +__version__ = '0.16.0' diff --git a/samcli/commands/build/build_context.py b/samcli/commands/build/build_context.py index dfed756294..0cb547f0ca 100644 --- a/samcli/commands/build/build_context.py +++ b/samcli/commands/build/build_context.py @@ -2,6 +2,7 @@ Context object used by build command """ +import logging import os import shutil @@ -14,6 +15,9 @@ from samcli.commands.local.lib.sam_function_provider import SamFunctionProvider from samcli.commands._utils.template import get_template_data from samcli.commands.exceptions import UserException +from samcli.local.lambdafn.exceptions import FunctionNotFound + +LOG = logging.getLogger(__name__) class BuildContext(object): @@ -23,6 +27,7 @@ class BuildContext(object): _BUILD_DIR_PERMISSIONS = 0o755 def __init__(self, + function_identifier, template_file, base_dir, build_dir, @@ -34,6 +39,7 @@ def __init__(self, docker_network=None, skip_pull_image=False): + self._function_identifier = function_identifier self._template_file = template_file self._base_dir = base_dir self._build_dir = build_dir @@ -128,3 +134,19 @@ def manifest_path_override(self): @property def mode(self): return self._mode + + @property + def functions_to_build(self): + if self._function_identifier: + function = self._function_provider.get(self._function_identifier) + + if not function: + all_functions = [f.name for f in self._function_provider.get_all()] + available_function_message = "{} not found. Possible options in your template: {}" \ + .format(self._function_identifier, all_functions) + LOG.info(available_function_message) + raise FunctionNotFound("Unable to find a Function with name '%s'", self._function_identifier) + + return [function] + + return self._function_provider.get_all() diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index 4c2df81fe8..f80dca0d83 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -14,6 +14,7 @@ from samcli.lib.build.app_builder import ApplicationBuilder, BuildError, UnsupportedBuilderLibraryVersionError, \ ContainerBuildNotSupported from samcli.lib.build.workflow_config import UnsupportedRuntimeException +from samcli.local.lambdafn.exceptions import FunctionNotFound from samcli.commands._utils.template import move_template LOG = logging.getLogger(__name__) @@ -32,7 +33,7 @@ Supported Runtimes ------------------ 1. Python 2.7, 3.6, 3.7 using PIP\n -2. Nodejs 8.10, 6.10 using NPM\n +2. Nodejs 10.x, 8.10, 6.10 using NPM\n 3. Ruby 2.5 using Bundler\n 4. Java 8 using Gradle\n 5. Dotnetcore2.0 and 2.1 using Dotnet CLI (without --use-container flag)\n @@ -81,8 +82,10 @@ @docker_common_options @cli_framework_options @aws_creds_options +@click.argument('function_identifier', required=False) @pass_context def cli(ctx, + function_identifier, template, base_dir, build_dir, @@ -96,11 +99,12 @@ def cli(ctx, mode = _get_mode_value_from_envvar("SAM_BUILD_MODE", choices=["debug"]) - do_cli(template, base_dir, build_dir, True, use_container, manifest, docker_network, + do_cli(function_identifier, template, base_dir, build_dir, True, use_container, manifest, docker_network, skip_pull_image, parameter_overrides, mode) # pragma: no cover -def do_cli(template, # pylint: disable=too-many-locals +def do_cli(function_identifier, # pylint: disable=too-many-locals + template, base_dir, build_dir, clean, @@ -119,7 +123,8 @@ def do_cli(template, # pylint: disable=too-many-locals if use_container: LOG.info("Starting Build inside a container") - with BuildContext(template, + with BuildContext(function_identifier, + template, base_dir, build_dir, clean=clean, @@ -129,14 +134,16 @@ def do_cli(template, # pylint: disable=too-many-locals docker_network=docker_network, skip_pull_image=skip_pull_image, mode=mode) as ctx: + try: + builder = ApplicationBuilder(ctx.functions_to_build, + ctx.build_dir, + ctx.base_dir, + manifest_path_override=ctx.manifest_path_override, + container_manager=ctx.container_manager, + mode=ctx.mode) + except FunctionNotFound as ex: + raise UserException(str(ex)) - builder = ApplicationBuilder(ctx.function_provider, - ctx.build_dir, - ctx.base_dir, - manifest_path_override=ctx.manifest_path_override, - container_manager=ctx.container_manager, - mode=ctx.mode - ) try: artifacts = builder.build() modified_template = builder.update_template(ctx.template_dict, diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index e015a08df5..90cf870900 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -103,7 +103,9 @@ def do_cli(ctx, location, runtime, dependency_manager, output_dir, name, no_inpu """.format(output_dir=output_dir, name=name) no_build_step_required = ( - "python", "python3.7", "python3.6", "python2.7", "nodejs", "nodejs4.3", "nodejs6.10", "nodejs8.10", "ruby2.5") + "python", "python3.7", "python3.6", "python2.7", "nodejs", + "nodejs4.3", "nodejs6.10", "nodejs8.10", "nodejs10.x", "ruby2.5" + ) next_step_msg = no_build_msg if runtime in no_build_step_required else build_msg try: diff --git a/samcli/commands/local/invoke/cli.py b/samcli/commands/local/invoke/cli.py index 071a5d5143..50c96e4d2a 100644 --- a/samcli/commands/local/invoke/cli.py +++ b/samcli/commands/local/invoke/cli.py @@ -14,7 +14,7 @@ from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException from samcli.commands.local.lib.exceptions import OverridesNotWellDefinedError from samcli.local.docker.manager import DockerImagePullFailedException -from samcli.local.docker.lambda_container import DebuggingNotSupported +from samcli.local.docker.lambda_debug_entrypoint import DebuggingNotSupported LOG = logging.getLogger(__name__) diff --git a/samcli/commands/local/start_api/cli.py b/samcli/commands/local/start_api/cli.py index 1a24c34908..b4928de7e6 100644 --- a/samcli/commands/local/start_api/cli.py +++ b/samcli/commands/local/start_api/cli.py @@ -13,7 +13,7 @@ from samcli.commands.local.lib.local_api_service import LocalApiService from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException from samcli.commands.local.lib.exceptions import OverridesNotWellDefinedError -from samcli.local.docker.lambda_container import DebuggingNotSupported +from samcli.local.docker.lambda_debug_entrypoint import DebuggingNotSupported LOG = logging.getLogger(__name__) diff --git a/samcli/commands/local/start_lambda/cli.py b/samcli/commands/local/start_lambda/cli.py index 6e7705d3b2..b27c479151 100644 --- a/samcli/commands/local/start_lambda/cli.py +++ b/samcli/commands/local/start_lambda/cli.py @@ -13,7 +13,7 @@ from samcli.commands.local.lib.local_lambda_service import LocalLambdaService from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException from samcli.commands.local.lib.exceptions import OverridesNotWellDefinedError -from samcli.local.docker.lambda_container import DebuggingNotSupported +from samcli.local.docker.lambda_debug_entrypoint import DebuggingNotSupported LOG = logging.getLogger(__name__) diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index 2ad2fe7b89..3ca2056769 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -49,7 +49,7 @@ class ApplicationBuilder(object): """ def __init__(self, - function_provider, + functions_to_build, build_dir, base_dir, manifest_path_override=None, @@ -61,8 +61,8 @@ def __init__(self, Parameters ---------- - function_provider : samcli.commands.local.lib.sam_function_provider.SamFunctionProvider - Provider that can vend out functions available in the SAM template + functions_to_build: Iterator + Iterator that can vend out functions available in the SAM template build_dir : str Path to the directory where we will be storing built artifacts @@ -79,7 +79,7 @@ def __init__(self, mode : str Optional, name of the build mode to use ex: 'debug' """ - self._function_provider = function_provider + self._functions_to_build = functions_to_build self._build_dir = build_dir self._base_dir = base_dir self._manifest_path_override = manifest_path_override @@ -100,7 +100,7 @@ def build(self): result = {} - for lambda_function in self._function_provider.get_all(): + for lambda_function in self._functions_to_build: LOG.info("Building resource '%s'", lambda_function.name) result[lambda_function.name] = self._build_function(lambda_function.name, diff --git a/samcli/lib/build/workflow_config.py b/samcli/lib/build/workflow_config.py index 28fe8e217e..475e036594 100644 --- a/samcli/lib/build/workflow_config.py +++ b/samcli/lib/build/workflow_config.py @@ -98,6 +98,7 @@ def get_workflow_config(runtime, code_dir, project_dir): "nodejs4.3": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs6.10": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs8.10": BasicWorkflowSelector(NODEJS_NPM_CONFIG), + "nodejs10.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "ruby2.5": BasicWorkflowSelector(RUBY_BUNDLER_CONFIG), "dotnetcore2.0": BasicWorkflowSelector(DOTNET_CLIPACKAGE_CONFIG), "dotnetcore2.1": BasicWorkflowSelector(DOTNET_CLIPACKAGE_CONFIG), diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index e1d2a9114a..91347743da 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -220,6 +220,14 @@ def _parse_lambda_output(lambda_output, binary_types, flask_request): LOG.error(message) raise TypeError(message) + # API Gateway only accepts statusCode, body, headers, and isBase64Encoded in + # a response shape. + invalid_keys = LocalApigwService._invalid_apig_response_keys(json_output) + if bool(invalid_keys): + msg = "Invalid API Gateway Response Keys: " + str(invalid_keys) + " in " + str(json_output) + LOG.error(msg) + raise ValueError(msg) + # If the customer doesn't define Content-Type default to application/json if "Content-Type" not in headers: LOG.info("No Content-Type given. Defaulting to 'application/json'.") @@ -230,6 +238,18 @@ def _parse_lambda_output(lambda_output, binary_types, flask_request): return status_code, headers, body + @staticmethod + def _invalid_apig_response_keys(output): + allowable = { + "statusCode", + "body", + "headers", + "isBase64Encoded" + } + # In Python 2.7, need to explicitly make the Dictionary keys into a set + invalid_keys = set(output.keys()) - allowable + return invalid_keys + @staticmethod def _should_base64_decode_body(binary_types, flask_request, lamba_response_headers, is_base_64_encoded): """ diff --git a/samcli/local/common/runtime_template.py b/samcli/local/common/runtime_template.py index 528732801b..ef67fe93ea 100644 --- a/samcli/local/common/runtime_template.py +++ b/samcli/local/common/runtime_template.py @@ -32,7 +32,7 @@ ], "nodejs": [ { - "runtimes": ["nodejs8.10"], + "runtimes": ["nodejs8.10", "nodejs10.x"], "dependency_manager": "npm", "init_location": os.path.join(_templates, "cookiecutter-aws-sam-hello-nodejs"), "build": True diff --git a/samcli/local/docker/lambda_container.py b/samcli/local/docker/lambda_container.py index 1ebb9806d0..a3bffa7f55 100644 --- a/samcli/local/docker/lambda_container.py +++ b/samcli/local/docker/lambda_container.py @@ -2,8 +2,8 @@ Represents Lambda runtime containers. """ import logging -import json +from samcli.local.docker.lambda_debug_entrypoint import LambdaDebugEntryPoint from .container import Container from .lambda_image import Runtime @@ -25,6 +25,9 @@ class LambdaContainer(Container): _DEBUGGER_VOLUME_MOUNT_PATH = "/tmp/lambci_debug_files" _DEFAULT_CONTAINER_DBG_GO_PATH = _DEBUGGER_VOLUME_MOUNT_PATH + "/dlv" + # Options for selecting debug entry point + _DEBUG_ENTRYPOINT_OPTIONS = {"delvePath": _DEFAULT_CONTAINER_DBG_GO_PATH} + # This is the dictionary that represents where the debugger_path arg is mounted in docker to as readonly. _DEBUGGER_VOLUME_MOUNT = {"bind": _DEBUGGER_VOLUME_MOUNT_PATH, "mode": "ro"} @@ -174,10 +177,6 @@ def _get_entry_point(runtime, debug_options=None): # pylint: disable=too-many-b if not debug_options: return None - if runtime not in LambdaContainer._supported_runtimes(): - raise DebuggingNotSupported( - "Debugging is not currently supported for {}".format(runtime)) - debug_port = debug_options.debug_port debug_args_list = [] @@ -186,130 +185,7 @@ def _get_entry_point(runtime, debug_options=None): # pylint: disable=too-many-b # configs from: https://github.com/lambci/docker-lambda # to which we add the extra debug mode options - entrypoint = None - if runtime == Runtime.java8.value: - - entrypoint = ["/usr/bin/java"] \ - + debug_args_list \ - + [ - "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=" + str(debug_port), - "-XX:MaxHeapSize=2834432k", - "-XX:MaxMetaspaceSize=163840k", - "-XX:ReservedCodeCacheSize=81920k", - "-XX:+UseSerialGC", - # "-Xshare:on", doesn't work in conjunction with the debug options - "-XX:-TieredCompilation", - "-Djava.net.preferIPv4Stack=true", - "-jar", - "/var/runtime/lib/LambdaJavaRTEntry-1.0.jar", - ] - - elif runtime in (Runtime.dotnetcore20.value, Runtime.dotnetcore21.value): - entrypoint = ["/var/lang/bin/dotnet"] \ - + debug_args_list \ - + [ - "/var/runtime/MockBootstraps.dll", - "--debugger-spin-wait" - ] - - elif runtime == Runtime.go1x.value: - entrypoint = ["/var/runtime/aws-lambda-go"] \ - + debug_args_list \ - + [ - "-debug=true", - "-delvePort=" + str(debug_port), - "-delvePath=" + LambdaContainer._DEFAULT_CONTAINER_DBG_GO_PATH, - ] - - elif runtime == Runtime.nodejs.value: - - entrypoint = ["/usr/bin/node"] \ - + debug_args_list \ - + [ - "--debug-brk=" + str(debug_port), - "--nolazy", - "--max-old-space-size=1229", - "--max-new-space-size=153", - "--max-executable-size=153", - "--expose-gc", - "/var/runtime/node_modules/awslambda/bin/awslambda", - ] - - elif runtime == Runtime.nodejs43.value: - - entrypoint = ["/usr/local/lib64/node-v4.3.x/bin/node"] \ - + debug_args_list \ - + [ - "--debug-brk=" + str(debug_port), - "--nolazy", - "--max-old-space-size=2547", - "--max-semi-space-size=150", - "--max-executable-size=160", - "--expose-gc", - "/var/runtime/node_modules/awslambda/index.js", - ] - - elif runtime == Runtime.nodejs610.value: - - entrypoint = ["/var/lang/bin/node"] \ - + debug_args_list \ - + [ - "--debug-brk=" + str(debug_port), - "--nolazy", - "--max-old-space-size=2547", - "--max-semi-space-size=150", - "--max-executable-size=160", - "--expose-gc", - "/var/runtime/node_modules/awslambda/index.js", - ] - - elif runtime == Runtime.nodejs810.value: - - entrypoint = ["/var/lang/bin/node"] \ - + debug_args_list \ - + [ - # Node8 requires the host to be explicitly set in order to bind to localhost - # instead of 127.0.0.1. https://github.com/nodejs/node/issues/11591#issuecomment-283110138 - "--inspect-brk=0.0.0.0:" + str(debug_port), - "--nolazy", - "--expose-gc", - "--max-semi-space-size=150", - "--max-old-space-size=2707", - "/var/runtime/node_modules/awslambda/index.js", - ] - - elif runtime == Runtime.python27.value: - - entrypoint = ["/usr/bin/python2.7"] \ - + debug_args_list \ - + [ - "/var/runtime/awslambda/bootstrap.py" - ] - - elif runtime == Runtime.python36.value: - - entrypoint = ["/var/lang/bin/python3.6"] \ - + debug_args_list \ - + [ - "/var/runtime/awslambda/bootstrap.py" - ] - - elif runtime == Runtime.python37.value: - entrypoint = ["/var/rapid/init", - "--bootstrap", - "/var/lang/bin/python3.7", - "--bootstrap-args", - json.dumps(debug_args_list + ["/var/runtime/bootstrap"]) - ] - - return entrypoint - - @staticmethod - def _supported_runtimes(): - return {Runtime.java8.value, Runtime.dotnetcore20.value, Runtime.dotnetcore21.value, Runtime.go1x.value, - Runtime.nodejs.value, Runtime.nodejs43.value, Runtime.nodejs610.value, Runtime.nodejs810.value, - Runtime.python27.value, Runtime.python36.value, Runtime.python37.value} - - -class DebuggingNotSupported(Exception): - pass + return LambdaDebugEntryPoint.get_entry_point(debug_port=debug_port, + debug_args_list=debug_args_list, + runtime=runtime, + options=LambdaContainer._DEBUG_ENTRYPOINT_OPTIONS) diff --git a/samcli/local/docker/lambda_debug_entrypoint.py b/samcli/local/docker/lambda_debug_entrypoint.py new file mode 100644 index 0000000000..f382adfa7c --- /dev/null +++ b/samcli/local/docker/lambda_debug_entrypoint.py @@ -0,0 +1,148 @@ +""" +Represents Lambda debug entrypoints. +""" + +import json + +from samcli.local.docker.lambda_image import Runtime + + +class DebuggingNotSupported(Exception): + pass + + +class LambdaDebugEntryPoint(object): + + @staticmethod + def get_entry_point(debug_port, debug_args_list, runtime, options): + + entrypoint_mapping = { + Runtime.java8.value: + ["/usr/bin/java"] + + debug_args_list + + [ + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=" + str(debug_port), + "-XX:MaxHeapSize=2834432k", + "-XX:MaxMetaspaceSize=163840k", + "-XX:ReservedCodeCacheSize=81920k", + "-XX:+UseSerialGC", + # "-Xshare:on", doesn't work in conjunction with the debug options + "-XX:-TieredCompilation", + "-Djava.net.preferIPv4Stack=true", + "-jar", + "/var/runtime/lib/LambdaJavaRTEntry-1.0.jar" + ], + + Runtime.dotnetcore20.value: + ["/var/lang/bin/dotnet"] + \ + debug_args_list + \ + [ + "/var/runtime/MockBootstraps.dll", + "--debugger-spin-wait" + ], + + Runtime.dotnetcore21.value: + ["/var/lang/bin/dotnet"] + \ + debug_args_list + \ + [ + "/var/runtime/MockBootstraps.dll", + "--debugger-spin-wait" + ], + Runtime.go1x.value: + ["/var/runtime/aws-lambda-go"] + \ + debug_args_list + \ + [ + "-debug=true", + "-delvePort=" + str(debug_port), + "-delvePath=" + options.get("delvePath"), + ], + Runtime.nodejs.value: + ["/usr/bin/node"] + \ + debug_args_list + \ + [ + "--debug-brk=" + str(debug_port), + "--nolazy", + "--max-old-space-size=1229", + "--max-new-space-size=153", + "--max-executable-size=153", + "--expose-gc", + "/var/runtime/node_modules/awslambda/bin/awslambda", + ], + Runtime.nodejs43.value: + ["/usr/local/lib64/node-v4.3.x/bin/node"] + \ + debug_args_list + \ + [ + "--debug-brk=" + str(debug_port), + "--nolazy", + "--max-old-space-size=2547", + "--max-semi-space-size=150", + "--max-executable-size=160", + "--expose-gc", + "/var/runtime/node_modules/awslambda/index.js", + ], + Runtime.nodejs610.value: + ["/var/lang/bin/node"] + \ + debug_args_list + \ + [ + "--debug-brk=" + str(debug_port), + "--nolazy", + "--max-old-space-size=2547", + "--max-semi-space-size=150", + "--max-executable-size=160", + "--expose-gc", + "/var/runtime/node_modules/awslambda/index.js", + ], + Runtime.nodejs810.value: + ["/var/lang/bin/node"] + \ + debug_args_list + \ + [ + # Node8 requires the host to be explicitly set in order to bind to localhost + # instead of 127.0.0.1. https://github.com/nodejs/node/issues/11591#issuecomment-283110138 + "--inspect-brk=0.0.0.0:" + str(debug_port), + "--nolazy", + "--expose-gc", + "--max-semi-space-size=150", + "--max-old-space-size=2707", + "/var/runtime/node_modules/awslambda/index.js", + ], + Runtime.nodejs10x.value: + ["/var/rapid/init", + "--bootstrap", + "/var/lang/bin/node", + "--bootstrap-args", + json.dumps(debug_args_list + + [ + "--inspect-brk=0.0.0.0:" + str(debug_port), + "--nolazy", + "--expose-gc", + "--max-http-header-size", + "81920", + "/var/runtime/index.js" + ] + ) + ], + Runtime.python27.value: + ["/usr/bin/python2.7"] + \ + debug_args_list + \ + [ + "/var/runtime/awslambda/bootstrap.py" + ], + Runtime.python36.value: + ["/var/lang/bin/python3.6"] + + debug_args_list + \ + [ + "/var/runtime/awslambda/bootstrap.py" + ], + Runtime.python37.value: + ["/var/rapid/init", + "--bootstrap", + "/var/lang/bin/python3.7", + "--bootstrap-args", + json.dumps(debug_args_list + ["/var/runtime/bootstrap"]) + ] + } + try: + return entrypoint_mapping[runtime] + except KeyError: + raise DebuggingNotSupported( + "Debugging is not currently supported for {}".format(runtime)) diff --git a/samcli/local/docker/lambda_image.py b/samcli/local/docker/lambda_image.py index 05c70980a1..9afc9d341f 100644 --- a/samcli/local/docker/lambda_image.py +++ b/samcli/local/docker/lambda_image.py @@ -25,6 +25,7 @@ class Runtime(Enum): nodejs43 = "nodejs4.3" nodejs610 = "nodejs6.10" nodejs810 = "nodejs8.10" + nodejs10x = "nodejs10.x" python27 = "python2.7" python36 = "python3.6" python37 = "python3.7" diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/HelloWorld.csproj b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/HelloWorld.csproj index bce2dcc23f..af452c53c4 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/HelloWorld.csproj +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/HelloWorld.csproj @@ -3,7 +3,7 @@ {%- if cookiecutter.runtime == 'dotnetcore2.0' %} netcoreapp2.0 - {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} + {%- else %} netcoreapp2.1 {%- endif %} true diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/aws-lambda-tools-defaults.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/aws-lambda-tools-defaults.json index 4e71062820..57c8d60741 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/aws-lambda-tools-defaults.json +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/aws-lambda-tools-defaults.json @@ -13,7 +13,7 @@ "configuration": "Release", {%- if cookiecutter.runtime == 'dotnetcore2.0' %} "framework": "netcoreapp2.0", - {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} + {%- else %} "framework" : "netcoreapp2.1", {%- endif %} "function-runtime":"{{ cookiecutter.runtime }}", diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/HelloWorld.Tests.csproj b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/HelloWorld.Tests.csproj index 50148be3a2..9e7851c6c0 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/HelloWorld.Tests.csproj +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/HelloWorld.Tests.csproj @@ -3,7 +3,7 @@ {%- if cookiecutter.runtime == 'dotnetcore2.0' %} netcoreapp2.0 - {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} + {%- else %} netcoreapp2.1 {%- endif %} diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/README.md index 614a9a5b7a..9222daa182 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/README.md @@ -10,6 +10,7 @@ A cookiecutter template to create a NodeJS Hello world boilerplate using [Server Generate a boilerplate template in your current project directory using the following syntax: +* **NodeJS 10**: `sam init --runtime nodejs10.x` * **NodeJS 8**: `sam init --runtime nodejs8.10` > **NOTE**: ``--name`` allows you to specify a different project folder name (`sam-app` is the default) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/cookiecutter.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/cookiecutter.json index 1462701ea8..60b6c69147 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/cookiecutter.json +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/cookiecutter.json @@ -1,4 +1,4 @@ { "project_name": "Name of the project", - "runtime": "nodejs8.10" + "runtime": "nodejs10.x" } \ No newline at end of file diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/README.md index ee97f83fbf..29c6a66896 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/README.md @@ -18,7 +18,12 @@ This is a sample template for {{ cookiecutter.project_name }} - Below is a brief ## Requirements * AWS CLI already configured with Administrator permission -* [NodeJS 8.10+ installed](https://nodejs.org/en/download/) +{%- if cookiecutter.runtime == 'nodejs8.10' %} +* [NodeJS 8.10+ installed](https://nodejs.org/en/download/releases/) +{%- else %} +* [NodeJS 10.10+ installed](https://nodejs.org/en/download/releases/) +{%- endif %} + * [Docker installed](https://www.docker.com/community-edition) ## Setup process diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/hello-world/package.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/hello-world/package.json index 6e5abce643..8f876665cc 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/hello-world/package.json +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/hello-world/package.json @@ -13,7 +13,7 @@ "test": "mocha tests/unit/" }, "devDependencies": { - "chai": "^4.1.2", - "mocha": "^5.1.1" + "chai": "^4.2.0", + "mocha": "^6.1.4" } } diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/template.yaml b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/template.yaml index a39cebc63c..3ffdbf536b 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/template.yaml +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/template.yaml @@ -16,7 +16,11 @@ Resources: Properties: CodeUri: hello-world/ Handler: app.lambdaHandler - Runtime: nodejs8.10 + {%- if cookiecutter.runtime == 'nodejs8.10' %} + Runtime: {{ cookiecutter.runtime }} + {%- else %} + Runtime: nodejs10.x + {%- endif %} Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api diff --git a/samcli/local/lambdafn/zip.py b/samcli/local/lambdafn/zip.py index 856dcbc4a4..cf4f07a8d0 100644 --- a/samcli/local/lambdafn/zip.py +++ b/samcli/local/lambdafn/zip.py @@ -107,7 +107,7 @@ def unzip_from_uri(uri, layer_zip_path, unzip_output_dir, progressbar_label): Label to use in the Progressbar """ try: - get_request = requests.get(uri, stream=True) + get_request = requests.get(uri, stream=True, verify=os.environ.get('AWS_CA_BUNDLE', True)) with open(layer_zip_path, 'wb') as local_layer_file: file_length = int(get_request.headers['Content-length']) diff --git a/tests/integration/buildcmd/build_integ_base.py b/tests/integration/buildcmd/build_integ_base.py index 5dd24e1d7c..703e7ff95d 100644 --- a/tests/integration/buildcmd/build_integ_base.py +++ b/tests/integration/buildcmd/build_integ_base.py @@ -20,6 +20,7 @@ class BuildIntegBase(TestCase): + template = "template.yaml" @classmethod def setUpClass(cls): @@ -34,7 +35,7 @@ def setUpClass(cls): cls.scratch_dir = str(Path(__file__).resolve().parent.joinpath("scratch")) cls.test_data_path = str(Path(integration_dir, "testdata", "buildcmd")) - cls.template_path = str(Path(cls.test_data_path, "template.yaml")) + cls.template_path = str(Path(cls.test_data_path, cls.template)) def setUp(self): @@ -61,9 +62,14 @@ def base_command(cls): return command def get_command_list(self, build_dir=None, base_dir=None, manifest_path=None, use_container=None, - parameter_overrides=None, mode=None): + parameter_overrides=None, mode=None, function_identifier=None): - command_list = [self.cmd, "build", "-t", self.template_path] + command_list = [self.cmd, "build"] + + if function_identifier: + command_list += [function_identifier] + + command_list += ["-t", self.template_path] if parameter_overrides: command_list += ["--parameter-overrides", self._make_parameter_override_arg(parameter_overrides)] diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index a1856b3f13..0ca79efb5a 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -86,7 +86,6 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files) function_logical_id) all_artifacts = set(os.listdir(str(resource_artifact_dir))) - print(all_artifacts) actual_files = all_artifacts.intersection(expected_files) self.assertEquals(actual_files, expected_files) @@ -122,9 +121,11 @@ class TestBuildCommand_NodeFunctions(BuildIntegBase): ("nodejs4.3", False), ("nodejs6.10", False), ("nodejs8.10", False), + ("nodejs10.x", False), ("nodejs4.3", "use_container"), ("nodejs6.10", "use_container"), - ("nodejs8.10", "use_container") + ("nodejs8.10", "use_container"), + ("nodejs10.x", "use_container") ]) def test_with_default_package_json(self, runtime, use_container): overrides = {"Runtime": runtime, "CodeUri": "Node", "Handler": "ignored"} @@ -402,3 +403,82 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files) all_artifacts = set(os.listdir(str(resource_artifact_dir))) actual_files = all_artifacts.intersection(expected_files) self.assertEquals(actual_files, expected_files) + + +class TestBuildCommand_SingleFunctionBuilds(BuildIntegBase): + template = "many-functions-template.yaml" + + EXPECTED_FILES_GLOBAL_MANIFEST = set() + EXPECTED_FILES_PROJECT_MANIFEST = {'__init__.py', 'main.py', 'numpy', + # 'cryptography', + "jinja2", + 'requirements.txt'} + + def test_function_not_found(self): + overrides = {"Runtime": 'python3.7', "CodeUri": "Python", "Handler": "main.handler"} + cmdlist = self.get_command_list(parameter_overrides=overrides, + function_identifier="FunctionNotInTemplate") + + process = subprocess.Popen(cmdlist, cwd=self.working_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + self.assertEquals(process.returncode, 1) + self.assertIn('FunctionNotInTemplate not found', str(stderr.decode('utf8'))) + + @parameterized.expand([ + ("python3.7", False, "FunctionOne"), + ("python3.7", "use_container", "FunctionOne"), + ("python3.7", False, "FunctionTwo"), + ("python3.7", "use_container", "FunctionTwo") + ]) + def test_build_single_function(self, runtime, use_container, function_identifier): + # Don't run test on wrong Python versions + py_version = self._get_python_version() + if py_version != runtime: + self.skipTest("Current Python version '{}' does not match Lambda runtime version '{}'".format(py_version, + runtime)) + + overrides = {"Runtime": runtime, "CodeUri": "Python", "Handler": "main.handler"} + cmdlist = self.get_command_list(use_container=use_container, + parameter_overrides=overrides, + function_identifier=function_identifier) + + LOG.info("Running Command: {}", cmdlist) + process = subprocess.Popen(cmdlist, cwd=self.working_dir) + process.wait() + + self._verify_built_artifact(self.default_build_dir, function_identifier, + self.EXPECTED_FILES_PROJECT_MANIFEST) + + expected = { + "pi": "3.14", + "jinja": "Hello World" + } + self._verify_invoke_built_function(self.built_template, + function_identifier, + self._make_parameter_override_arg(overrides), + expected) + self.verify_docker_container_cleanedup(runtime) + + def _verify_built_artifact(self, build_dir, function_logical_id, expected_files): + self.assertTrue(build_dir.exists(), "Build directory should be created") + + build_dir_files = os.listdir(str(build_dir)) + self.assertIn("template.yaml", build_dir_files) + self.assertIn(function_logical_id, build_dir_files) + + template_path = build_dir.joinpath("template.yaml") + resource_artifact_dir = build_dir.joinpath(function_logical_id) + + # Make sure the template has correct CodeUri for resource + self._verify_resource_property(str(template_path), + function_logical_id, + "CodeUri", + function_logical_id) + + all_artifacts = set(os.listdir(str(resource_artifact_dir))) + actual_files = all_artifacts.intersection(expected_files) + self.assertEquals(actual_files, expected_files) + + def _get_python_version(self): + return "python{}.{}".format(sys.version_info.major, sys.version_info.minor) diff --git a/tests/integration/local/start_api/test_start_api.py b/tests/integration/local/start_api/test_start_api.py index 577c6acbcb..dc8971cddb 100644 --- a/tests/integration/local/start_api/test_start_api.py +++ b/tests/integration/local/start_api/test_start_api.py @@ -86,6 +86,12 @@ def test_invalid_response_from_lambda(self): self.assertEquals(response.status_code, 502) self.assertEquals(response.json(), {"message": "Internal server error"}) + def test_invalid_json_response_from_lambda(self): + response = requests.get(self.url + "/invalidresponsehash") + + self.assertEquals(response.status_code, 502) + self.assertEquals(response.json(), {"message": "Internal server error"}) + def test_request_timeout(self): pass diff --git a/tests/integration/testdata/buildcmd/many-functions-template.yaml b/tests/integration/testdata/buildcmd/many-functions-template.yaml new file mode 100644 index 0000000000..1ef004b02d --- /dev/null +++ b/tests/integration/testdata/buildcmd/many-functions-template.yaml @@ -0,0 +1,28 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Parameteres: + Runtime: + Type: String + CodeUri: + Type: String + Handler: + Type: String + +Resources: + + FunctionOne: + Type: AWS::Serverless::Function + Properties: + Handler: !Ref Handler + Runtime: !Ref Runtime + CodeUri: !Ref CodeUri + Timeout: 600 + + FunctionTwo: + Type: AWS::Serverless::Function + Properties: + Handler: !Ref Handler + Runtime: !Ref Runtime + CodeUri: !Ref CodeUri + Timeout: 600 diff --git a/tests/integration/testdata/start_api/main.py b/tests/integration/testdata/start_api/main.py index 9304ec8495..127e758981 100644 --- a/tests/integration/testdata/start_api/main.py +++ b/tests/integration/testdata/start_api/main.py @@ -63,6 +63,10 @@ def invalid_response_returned(event, context): return "This is invalid" +def invalid_hash_response(event, context): + return {"foo": "bar"} + + def base64_response(event, context): gifImageBase64 = "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" # NOQA diff --git a/tests/integration/testdata/start_api/template.yaml b/tests/integration/testdata/start_api/template.yaml index d3ec04ec57..8820753c47 100644 --- a/tests/integration/testdata/start_api/template.yaml +++ b/tests/integration/testdata/start_api/template.yaml @@ -191,6 +191,19 @@ Resources: Method: GET Path: /invalidresponsereturned + InvalidResponseHashFromLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.invalid_hash_response + Runtime: python3.6 + CodeUri: . + Events: + InvalidResponseReturned: + Type: Api + Properties: + Method: GET + Path: /invalidresponsehash + Base64ResponseFunction: Type: AWS::Serverless::Function Properties: diff --git a/tests/unit/commands/buildcmd/test_build_context.py b/tests/unit/commands/buildcmd/test_build_context.py index ce9e413cef..cf68e2ed9d 100644 --- a/tests/unit/commands/buildcmd/test_build_context.py +++ b/tests/unit/commands/buildcmd/test_build_context.py @@ -15,11 +15,14 @@ def test_must_setup_context(self, ContainerManagerMock, pathlib_mock, SamFunctio get_template_data_mock): template_dict = get_template_data_mock.return_value = "template dict" - funcprovider = SamFunctionProviderMock.return_value = "funcprovider" + func_provider_mock = Mock() + func_provider_mock.get.return_value = "function to build" + funcprovider = SamFunctionProviderMock.return_value = func_provider_mock base_dir = pathlib_mock.Path.return_value.resolve.return_value.parent = "basedir" container_mgr_mock = ContainerManagerMock.return_value = Mock() - context = BuildContext("template_file", + context = BuildContext("function_identifier", + "template_file", None, # No base dir is provided "build_dir", manifest_path="manifest_path", @@ -46,6 +49,7 @@ def test_must_setup_context(self, ContainerManagerMock, pathlib_mock, SamFunctio self.assertEquals(context.output_template_path, os.path.join(build_dir_result, "template.yaml")) self.assertEquals(context.manifest_path_override, os.path.abspath("manifest_path")) self.assertEqual(context.mode, "buildmode") + self.assertEquals(context.functions_to_build, ["function to build"]) get_template_data_mock.assert_called_once_with("template_file") SamFunctionProviderMock.assert_called_once_with(template_dict, "overrides") @@ -53,6 +57,58 @@ def test_must_setup_context(self, ContainerManagerMock, pathlib_mock, SamFunctio setup_build_dir_mock.assert_called_with("build_dir", True) ContainerManagerMock.assert_called_once_with(docker_network_id="network", skip_pull_image=True) + func_provider_mock.get.assert_called_once_with("function_identifier") + + @patch("samcli.commands.build.build_context.get_template_data") + @patch("samcli.commands.build.build_context.SamFunctionProvider") + @patch("samcli.commands.build.build_context.pathlib") + @patch("samcli.commands.build.build_context.ContainerManager") + def test_must_return_many_functions_to_build(self, ContainerManagerMock, pathlib_mock, SamFunctionProviderMock, + get_template_data_mock): + template_dict = get_template_data_mock.return_value = "template dict" + func_provider_mock = Mock() + func_provider_mock.get_all.return_value = ["function to build", "and another function"] + funcprovider = SamFunctionProviderMock.return_value = func_provider_mock + base_dir = pathlib_mock.Path.return_value.resolve.return_value.parent = "basedir" + container_mgr_mock = ContainerManagerMock.return_value = Mock() + + context = BuildContext(None, + "template_file", + None, # No base dir is provided + "build_dir", + manifest_path="manifest_path", + clean=True, + use_container=True, + docker_network="network", + parameter_overrides="overrides", + skip_pull_image=True, + mode="buildmode") + setup_build_dir_mock = Mock() + build_dir_result = setup_build_dir_mock.return_value = "my/new/build/dir" + context._setup_build_dir = setup_build_dir_mock + + # call the enter method + result = context.__enter__() + + self.assertEquals(result, context) # __enter__ must return self + self.assertEquals(context.template_dict, template_dict) + self.assertEquals(context.function_provider, funcprovider) + self.assertEquals(context.base_dir, base_dir) + self.assertEquals(context.container_manager, container_mgr_mock) + self.assertEquals(context.build_dir, build_dir_result) + self.assertEquals(context.use_container, True) + self.assertEquals(context.output_template_path, os.path.join(build_dir_result, "template.yaml")) + self.assertEquals(context.manifest_path_override, os.path.abspath("manifest_path")) + self.assertEqual(context.mode, "buildmode") + self.assertEquals(context.functions_to_build, ["function to build", "and another function"]) + + get_template_data_mock.assert_called_once_with("template_file") + SamFunctionProviderMock.assert_called_once_with(template_dict, "overrides") + pathlib_mock.Path.assert_called_once_with("template_file") + setup_build_dir_mock.assert_called_with("build_dir", True) + ContainerManagerMock.assert_called_once_with(docker_network_id="network", + skip_pull_image=True) + func_provider_mock.get_all.assert_called_once() class TestBuildContext_setup_build_dir(TestCase): diff --git a/tests/unit/commands/buildcmd/test_command.py b/tests/unit/commands/buildcmd/test_command.py index 8b867fe480..1df0bbf946 100644 --- a/tests/unit/commands/buildcmd/test_command.py +++ b/tests/unit/commands/buildcmd/test_command.py @@ -9,6 +9,7 @@ from samcli.commands.exceptions import UserException from samcli.lib.build.app_builder import BuildError, UnsupportedBuilderLibraryVersionError from samcli.lib.build.workflow_config import UnsupportedRuntimeException +from samcli.local.lambdafn.exceptions import FunctionNotFound class TestDoCli(TestCase): @@ -30,10 +31,10 @@ def test_must_succeed_build(self, artifacts = builder_mock.build.return_value = "artifacts" modified_template = builder_mock.update_template.return_value = "modified template" - do_cli("template", "base_dir", "build_dir", "clean", "use_container", + do_cli("function_identifier", "template", "base_dir", "build_dir", "clean", "use_container", "manifest_path", "docker_network", "skip_pull", "parameter_overrides", "mode") - ApplicationBuilderMock.assert_called_once_with(ctx_mock.function_provider, + ApplicationBuilderMock.assert_called_once_with(ctx_mock.functions_to_build, ctx_mock.build_dir, ctx_mock.base_dir, manifest_path_override=ctx_mock.manifest_path_override, @@ -64,11 +65,25 @@ def test_must_catch_known_exceptions(self, exception, ApplicationBuilderMock, Bu builder_mock.build.side_effect = exception with self.assertRaises(UserException) as ctx: - do_cli("template", "base_dir", "build_dir", "clean", "use_container", + do_cli("function_identifier", "template", "base_dir", "build_dir", "clean", "use_container", "manifest_path", "docker_network", "skip_pull", "parameteroverrides", "mode") self.assertEquals(str(ctx.exception), str(exception)) + @patch("samcli.commands.build.command.BuildContext") + @patch("samcli.commands.build.command.ApplicationBuilder") + def test_must_catch_function_not_found_exception(self, ApplicationBuilderMock, BuildContextMock): + ctx_mock = Mock() + BuildContextMock.return_value.__enter__ = Mock() + BuildContextMock.return_value.__enter__.return_value = ctx_mock + ApplicationBuilderMock.side_effect = FunctionNotFound('Function Not Found') + + with self.assertRaises(UserException) as ctx: + do_cli("function_identifier", "template", "base_dir", "build_dir", "clean", "use_container", + "manifest_path", "docker_network", "skip_pull", "parameteroverrides", "mode") + + self.assertEquals(str(ctx.exception), 'Function Not Found') + class TestGetModeValueFromEnvvar(TestCase): diff --git a/tests/unit/commands/local/invoke/test_cli.py b/tests/unit/commands/local/invoke/test_cli.py index b4fdf459e8..8f6b514b36 100644 --- a/tests/unit/commands/local/invoke/test_cli.py +++ b/tests/unit/commands/local/invoke/test_cli.py @@ -13,7 +13,7 @@ from samcli.commands.local.invoke.cli import do_cli as invoke_cli, _get_event as invoke_cli_get_event from samcli.commands.local.lib.exceptions import OverridesNotWellDefinedError from samcli.local.docker.manager import DockerImagePullFailedException -from samcli.local.docker.lambda_container import DebuggingNotSupported +from samcli.local.docker.lambda_debug_entrypoint import DebuggingNotSupported STDIN_FILE_NAME = "-" diff --git a/tests/unit/commands/local/start_api/test_cli.py b/tests/unit/commands/local/start_api/test_cli.py index 13e2e02560..8dbb129ddb 100644 --- a/tests/unit/commands/local/start_api/test_cli.py +++ b/tests/unit/commands/local/start_api/test_cli.py @@ -12,7 +12,7 @@ from samcli.commands.exceptions import UserException from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException from samcli.commands.local.lib.exceptions import OverridesNotWellDefinedError -from samcli.local.docker.lambda_container import DebuggingNotSupported +from samcli.local.docker.lambda_debug_entrypoint import DebuggingNotSupported class TestCli(TestCase): diff --git a/tests/unit/commands/local/start_lambda/test_cli.py b/tests/unit/commands/local/start_lambda/test_cli.py index cc22295abb..3656f8245c 100644 --- a/tests/unit/commands/local/start_lambda/test_cli.py +++ b/tests/unit/commands/local/start_lambda/test_cli.py @@ -8,7 +8,7 @@ from samcli.commands.local.cli_common.user_exceptions import UserException from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException from samcli.commands.local.lib.exceptions import OverridesNotWellDefinedError -from samcli.local.docker.lambda_container import DebuggingNotSupported +from samcli.local.docker.lambda_debug_entrypoint import DebuggingNotSupported class TestCli(TestCase): diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index a9c628f00d..8450af2e51 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -14,29 +14,27 @@ class TestApplicationBuilder_build(TestCase): def setUp(self): - self.mock_func_provider = Mock() - self.builder = ApplicationBuilder(self.mock_func_provider, + self.func1 = Mock() + self.func2 = Mock() + self.builder = ApplicationBuilder([self.func1, self.func2], "builddir", "basedir") def test_must_iterate_on_functions(self): - func1 = Mock() - func2 = Mock() build_function_mock = Mock() - self.mock_func_provider.get_all.return_value = [func1, func2] self.builder._build_function = build_function_mock result = self.builder.build() self.assertEquals(result, { - func1.name: build_function_mock.return_value, - func2.name: build_function_mock.return_value, + self.func1.name: build_function_mock.return_value, + self.func2.name: build_function_mock.return_value, }) build_function_mock.assert_has_calls([ - call(func1.name, func1.codeuri, func1.runtime), - call(func2.name, func2.codeuri, func2.runtime), + call(self.func1.name, self.func1.codeuri, self.func1.runtime), + call(self.func2.name, self.func2.codeuri, self.func2.runtime), ], any_order=False) diff --git a/tests/unit/local/apigw/test_local_apigw_service.py b/tests/unit/local/apigw/test_local_apigw_service.py index 712674ff36..68ee809982 100644 --- a/tests/unit/local/apigw/test_local_apigw_service.py +++ b/tests/unit/local/apigw/test_local_apigw_service.py @@ -289,17 +289,14 @@ def test_custom_content_type_header_is_not_modified(self): self.assertIn("Content-Type", headers) self.assertEquals(headers["Content-Type"], "text/xml") - def test_extra_values_ignored(self): + def test_extra_values_raise(self): lambda_output = '{"statusCode": 200, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ '"isBase64Encoded": false, "another_key": "some value"}' - (status_code, headers, body) = LocalApigwService._parse_lambda_output(lambda_output, - binary_types=[], - flask_request=Mock()) - - self.assertEquals(status_code, 200) - self.assertEquals(headers, {"Content-Type": "application/json"}) - self.assertEquals(body, '{"message":"Hello from Lambda"}') + with self.assertRaises(ValueError): + LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) def test_parse_returns_correct_tuple(self): lambda_output = '{"statusCode": 200, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ diff --git a/tests/unit/local/docker/test_lambda_container.py b/tests/unit/local/docker/test_lambda_container.py index 31df5d7b46..963e268a0b 100644 --- a/tests/unit/local/docker/test_lambda_container.py +++ b/tests/unit/local/docker/test_lambda_container.py @@ -7,7 +7,8 @@ from parameterized import parameterized, param from samcli.commands.local.lib.debug_context import DebugContext -from samcli.local.docker.lambda_container import LambdaContainer, Runtime, DebuggingNotSupported +from samcli.local.docker.lambda_container import LambdaContainer, Runtime +from samcli.local.docker.lambda_debug_entrypoint import DebuggingNotSupported RUNTIMES_WITH_ENTRYPOINT = [Runtime.java8.value, Runtime.dotnetcore20.value, @@ -20,6 +21,10 @@ Runtime.python36.value, Runtime.python27.value] +RUNTIMES_WITH_BOOTSTRAP_ENTRYPOINT = [Runtime.nodejs10x.value, + Runtime.python37.value] + + ALL_RUNTIMES = [r for r in Runtime] @@ -145,7 +150,7 @@ def test_must_provide_entrypoint_for_certain_runtimes_only(self, runtime): with self.assertRaises(DebuggingNotSupported): LambdaContainer._get_entry_point(runtime, self.debug_options) - @parameterized.expand([param(r) for r in RUNTIMES_WITH_ENTRYPOINT]) + @parameterized.expand([param(r) for r in set(RUNTIMES_WITH_ENTRYPOINT)]) def test_debug_arg_must_be_split_by_spaces_and_appended_to_entrypoint(self, runtime): """ Debug args list is appended starting at second position in the array @@ -156,6 +161,17 @@ def test_debug_arg_must_be_split_by_spaces_and_appended_to_entrypoint(self, runt self.assertEquals(actual, expected_debug_args) + @parameterized.expand([param(r) for r in set(RUNTIMES_WITH_BOOTSTRAP_ENTRYPOINT)]) + def test_debug_arg_must_be_split_by_spaces_and_appended_to_bootstrap_based_entrypoint(self, runtime): + """ + Debug args list is appended as arguments to bootstrap-args, which is past the fourth position in the array + """ + expected_debug_args = ["a=b", "c=d", "e=f"] + result = LambdaContainer._get_entry_point(runtime, self.debug_options) + actual = result[4:5][0] + + self.assertTrue(all(debug_arg in actual for debug_arg in expected_debug_args)) + @parameterized.expand([param(r) for r in RUNTIMES_WITH_ENTRYPOINT]) def test_must_provide_entrypoint_even_without_debug_args(self, runtime): debug_options = DebugContext(debug_port=1235, debug_args=None) diff --git a/tests/unit/local/lambdafn/test_zip.py b/tests/unit/local/lambdafn/test_zip.py index 973990a500..2b8dfb355d 100644 --- a/tests/unit/local/lambdafn/test_zip.py +++ b/tests/unit/local/lambdafn/test_zip.py @@ -86,7 +86,14 @@ class TestUnzipFromUri(TestCase): @patch('samcli.local.lambdafn.zip.progressbar') @patch('samcli.local.lambdafn.zip.requests') @patch('samcli.local.lambdafn.zip.open') - def test_successfully_unzip_from_uri(self, open_patch, requests_patch, progressbar_patch, path_patch, unzip_patch): + @patch('samcli.local.lambdafn.zip.os') + def test_successfully_unzip_from_uri(self, + os_patch, + open_patch, + requests_patch, + progressbar_patch, + path_patch, + unzip_patch): get_request_mock = Mock() get_request_mock.headers = {"Content-length": "200"} get_request_mock.iter_content.return_value = [b'data1'] @@ -102,9 +109,11 @@ def test_successfully_unzip_from_uri(self, open_patch, requests_patch, progressb path_mock.exists.return_value = True path_patch.return_value = path_mock + os_patch.environ.get.return_value = True + unzip_from_uri('uri', 'layer_zip_path', 'output_zip_dir', 'layer_arn') - requests_patch.get.assert_called_with('uri', stream=True) + requests_patch.get.assert_called_with('uri', stream=True, verify=True) get_request_mock.iter_content.assert_called_with(chunk_size=None) open_patch.assert_called_with('layer_zip_path', 'wb') file_mock.write.assert_called_with(b'data1') @@ -112,13 +121,16 @@ def test_successfully_unzip_from_uri(self, open_patch, requests_patch, progressb path_patch.assert_called_with('layer_zip_path') path_mock.unlink.assert_called() unzip_patch.assert_called_with('layer_zip_path', 'output_zip_dir', permission=0o700) + os_patch.environ.get.assert_called_with('AWS_CA_BUNDLE', True) @patch('samcli.local.lambdafn.zip.unzip') @patch('samcli.local.lambdafn.zip.Path') @patch('samcli.local.lambdafn.zip.progressbar') @patch('samcli.local.lambdafn.zip.requests') @patch('samcli.local.lambdafn.zip.open') + @patch('samcli.local.lambdafn.zip.os') def test_not_unlink_file_when_file_doesnt_exist(self, + os_patch, open_patch, requests_patch, progressbar_patch, @@ -139,9 +151,11 @@ def test_not_unlink_file_when_file_doesnt_exist(self, path_mock.exists.return_value = False path_patch.return_value = path_mock + os_patch.environ.get.return_value = True + unzip_from_uri('uri', 'layer_zip_path', 'output_zip_dir', 'layer_arn') - requests_patch.get.assert_called_with('uri', stream=True) + requests_patch.get.assert_called_with('uri', stream=True, verify=True) get_request_mock.iter_content.assert_called_with(chunk_size=None) open_patch.assert_called_with('layer_zip_path', 'wb') file_mock.write.assert_called_with(b'data1') @@ -149,6 +163,48 @@ def test_not_unlink_file_when_file_doesnt_exist(self, path_patch.assert_called_with('layer_zip_path') path_mock.unlink.assert_not_called() unzip_patch.assert_called_with('layer_zip_path', 'output_zip_dir', permission=0o700) + os_patch.environ.get.assert_called_with('AWS_CA_BUNDLE', True) + + @patch('samcli.local.lambdafn.zip.unzip') + @patch('samcli.local.lambdafn.zip.Path') + @patch('samcli.local.lambdafn.zip.progressbar') + @patch('samcli.local.lambdafn.zip.requests') + @patch('samcli.local.lambdafn.zip.open') + @patch('samcli.local.lambdafn.zip.os') + def test_unzip_from_uri_reads_AWS_CA_BUNDLE_env_var(self, os_patch, + open_patch, + requests_patch, + progressbar_patch, + path_patch, + unzip_patch): + get_request_mock = Mock() + get_request_mock.headers = {"Content-length": "200"} + get_request_mock.iter_content.return_value = [b'data1'] + requests_patch.get.return_value = get_request_mock + + file_mock = Mock() + open_patch.return_value.__enter__.return_value = file_mock + + progressbar_mock = Mock() + progressbar_patch.return_value.__enter__.return_value = progressbar_mock + + path_mock = Mock() + path_mock.exists.return_value = True + path_patch.return_value = path_mock + + os_patch.environ.get.return_value = '/some/path/on/the/system' + + unzip_from_uri('uri', 'layer_zip_path', 'output_zip_dir', 'layer_arn') + + requests_patch.get.assert_called_with('uri', stream=True, verify='/some/path/on/the/system') + get_request_mock.iter_content.assert_called_with(chunk_size=None) + open_patch.assert_called_with('layer_zip_path', 'wb') + file_mock.write.assert_called_with(b'data1') + progressbar_mock.update.assert_called_with(5) + path_patch.assert_called_with('layer_zip_path') + path_mock.unlink.assert_called() + unzip_patch.assert_called_with('layer_zip_path', 'output_zip_dir', permission=0o700) + os_patch.environ.get.assert_called_with('AWS_CA_BUNDLE', True) class TestOverridePermissions(TestCase):