From 49cb854d3f04ecbd59a562ddd5285b31da3dfc9e Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan <3770774+sriram-mv@users.noreply.github.com> Date: Wed, 20 May 2020 09:11:03 -0700 Subject: [PATCH 1/3] feat: allow a custom builder workflow selection (#1957) * feat: allow a custom builder workflow selection Why is this change necessary? * Build for provided runtimes and for existing runtimes to have thier own build process. How does it address the issue? * Alternative build mechanisms for those usecases where existing build mechanisms dont solve the usecase. What side effects does this change have? * Metadata is updated as part of function properties, for easy wiring. Have not seen side-effects in current testing yet. * design: add explicit design doc within sam cli - component design already present within `aws-lambda-builders`: https://github.com/awslabs/aws-lambda-builders/blob/develop/aws_lambda_builders/workflows/custom_make/DESIGN.md --- appveyor-windows.yml | 3 + designs/build_for_provided_runtimes.md | 139 +++++++++++++ samcli/lib/build/app_builder.py | 28 ++- samcli/lib/build/workflow_config.py | 79 ++++++- samcli/lib/providers/provider.py | 2 + samcli/lib/providers/sam_function_provider.py | 5 + tests/integration/buildcmd/test_build_cmd.py | 193 ++++++++++++++++++ .../testdata/buildcmd/Provided/Makefile | 5 + .../buildcmd/Provided/Makefile-container | 7 + .../testdata/buildcmd/Provided/__init__.py | 0 .../testdata/buildcmd/Provided/main.py | 4 + .../buildcmd/Provided/requirements.txt | 1 + .../buildcmd/custom-build-function.yaml | 25 +++ .../commands/local/lib/test_local_lambda.py | 5 + .../local/lib/test_sam_function_provider.py | 13 ++ .../unit/lib/build_module/test_app_builder.py | 34 ++- .../lib/build_module/test_workflow_config.py | 36 +++- 17 files changed, 557 insertions(+), 22 deletions(-) create mode 100644 designs/build_for_provided_runtimes.md create mode 100644 tests/integration/testdata/buildcmd/Provided/Makefile create mode 100644 tests/integration/testdata/buildcmd/Provided/Makefile-container create mode 100644 tests/integration/testdata/buildcmd/Provided/__init__.py create mode 100644 tests/integration/testdata/buildcmd/Provided/main.py create mode 100644 tests/integration/testdata/buildcmd/Provided/requirements.txt create mode 100644 tests/integration/testdata/buildcmd/custom-build-function.yaml diff --git a/appveyor-windows.yml b/appveyor-windows.yml index 0296ed6b7b..355131b96c 100644 --- a/appveyor-windows.yml +++ b/appveyor-windows.yml @@ -40,6 +40,9 @@ cache: install: + # setup make + - "choco install make" + # Make sure the temp directory exists for Python to use. - ps: "mkdir -Force D:\\tmp" - "SET PATH=%PYTHON_HOME%;%PATH%" diff --git a/designs/build_for_provided_runtimes.md b/designs/build_for_provided_runtimes.md new file mode 100644 index 0000000000..e031af20ca --- /dev/null +++ b/designs/build_for_provided_runtimes.md @@ -0,0 +1,139 @@ +What is the problem? +-------------------- +* sam build does not support building for `provided` runtimes. +* sam build also does not allow customization of the build process for the supported lambda runtimes. + +What will be changed? +--------------------- + +Serverless Function resources can now have a Metadata Resource Attribute which specifies a `BuildMethod`. +`BuildMethod` will either be the official lambda runtime identifiers such as `python3.8`, `nodejs12.x` etc or `makefile`. +If `BuildMethod` is specified to be `makefile`, the build targets that are present in the `Makefile` which take the form of + +`build-{resource_logical_id}` will be executed. + +More details can also be found at: [CustomMakeBuildWorkflow](https://github.com/awslabs/aws-lambda-builders/blob/develop/aws_lambda_builders/workflows/custom_make/DESIGN.md) + +This enables following usecases: + +* build for `provided` runtimes. +* user specified build steps for official lambda supported runtimes instead of what `sam build` natively offers. + +Success criteria for the change +------------------------------- + +* Users are able to build for `provided` runtimes through sam build directly. +* Users are able to bring their own build steps for even natively supported lambda runtimes. + +User Experience Walkthrough +--------------------------- + +#### Provided runtimes + +Template + +```yaml +Resources: + HelloRustFunction: + Type: AWS::Serverless::Function + Properties: + Handler: bootstrap.is.real.handler + Runtime: provided + MemorySize: 512 + CodeUri: . + Metadata: + BuildMethod: makefile +``` + +Makefile + +``` +build-HelloRustFunction: + cargo build --release --target x86_64-unknown-linux-musl + cp ./target/x86_64-unknown-linux-musl/release/bootstrap $(ARTIFACTS_DIR) +``` + +#### Makefile builder for lambda runtimes + +Template + +```yaml +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler + Runtime: python3.7 + Metadata: + BuildMethod: makefile +``` + +Makefile + +``` +build-HelloWorldFunction: + cp *.py $(ARTIFACTS_DIR) + cp requirements.txt $(ARTIFACTS_DIR) + python -m pip install -r requirements.txt -t $(ARTIFACTS_DIR) + rm -rf $(ARTIFACTS_DIR)/bin +``` + +Implementation +============== +### Proposal + +* Selection of the build workflow within sam cli will now have additional logic to select the correct build workflow from user input in the template. +Currently, a build workflow is chosen based on the lambda runtime and the manifest file alone. + +FAQs +---- +1. Can a user specify the BuildMethod to be `python3.8` for a `python3.8` runtime? + * Yes, this will just select the native python workflow that is used from `aws-lambda-builders` +2. Can a user specify BuildMethod to be `ruby2.7` for a `python3.8` runtime? + * Theoretically yes, But the build will just fail. +3. Can a user specify the BuildMethod to be `python3.7` for a `python3.8` runtime? + * Theoretically yes, However If the user is just using the builder that samcli already provides, + its best not to provide any `BuildMethod` at all. + + +CLI Changes +----------- + +No changes in CLI interface. + +### Breaking Change + +No breaking changes. + + +What is your Testing Plan (QA)? +=============================== + +* Unit and Integration testing + +Goal +---- + +* Coverage of usecases such as: + * build for `provided` runtimes + * build for official lambda runtimes through the `makefile` construct + * build within containers for the `makefile` construct + + +Expected Results +---------------- +* Integration tests to pass that covers usecases listed in the goal. + + +Task Breakdown +============== + +- \[x\] Send a Pull Request with this design document +- \[ \] Build the command line interface +- \[ \] Build the underlying library +- \[ \] Unit tests +- \[ \] Functional Tests +- \[ \] Integration tests +- \[ \] Run all tests on Windows +- \[ \] Update documentation diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index 2888bd0249..e1f9d9c903 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -113,7 +113,8 @@ def build(self): result[function.name] = self._build_function(function.name, function.codeuri, function.runtime, - function.handler) + function.handler, + function.metadata) for layer in self._resources_to_build.layers: LOG.info("Building layer '%s'", layer.name) if layer.build_method is None: @@ -200,7 +201,7 @@ def _build_layer(self, layer_name, codeuri, runtime): # Not including subfolder in return so that we copy subfolder, instead of copying artifacts inside it. return str(pathlib.Path(self._build_dir, layer_name)) - def _build_function(self, function_name, codeuri, runtime, handler): + def _build_function(self, function_name, codeuri, runtime, handler, metadata=None): """ Given the function information, this method will build the Lambda function. Depending on the configuration it will either build the function in process or by spinning up a Docker container. @@ -216,6 +217,9 @@ def _build_function(self, function_name, codeuri, runtime, handler): runtime : str AWS Lambda function runtime + metadata : dict + AWS Lambda function metadata + Returns ------- str @@ -232,7 +236,10 @@ def _build_function(self, function_name, codeuri, runtime, handler): # Code is always relative to the given base directory. code_dir = str(pathlib.Path(self._base_dir, codeuri).resolve()) - config = get_workflow_config(runtime, code_dir, self._base_dir) + # Determine if there was a build workflow that was specified directly in the template. + specified_build_workflow = metadata.get("BuildMethod", None) if metadata else None + + config = get_workflow_config(runtime, code_dir, self._base_dir, specified_workflow=specified_build_workflow) # artifacts directory will be created by the builder artifacts_dir = str(pathlib.Path(self._build_dir, function_name)) @@ -245,7 +252,7 @@ def _build_function(self, function_name, codeuri, runtime, handler): if self._container_manager: build_method = self._build_function_on_container - options = ApplicationBuilder._get_build_options(config.language, handler) + options = ApplicationBuilder._get_build_options(function_name, config.language, handler) return build_method(config, code_dir, @@ -256,12 +263,14 @@ def _build_function(self, function_name, codeuri, runtime, handler): options) @staticmethod - def _get_build_options(language, handler): + def _get_build_options(function_name, language, handler): """ Parameters ---------- + function_name str + currrent function resource name language str - Language of the runtime + language of the runtime handler str Handler value of the Lambda Function Resource Returns @@ -269,7 +278,12 @@ def _get_build_options(language, handler): dict Dictionary that represents the options to pass to the builder workflow or None if options are not needed """ - return {'artifact_executable_name': handler} if language == 'go' else None + + _build_options = { + 'go': {'artifact_executable_name': handler}, + 'provided': {'build_logical_id': function_name} + } + return _build_options.get(language, None) def _build_function_in_process(self, config, diff --git a/samcli/lib/build/workflow_config.py b/samcli/lib/build/workflow_config.py index 7853c84a65..2d6d0335cf 100644 --- a/samcli/lib/build/workflow_config.py +++ b/samcli/lib/build/workflow_config.py @@ -67,10 +67,63 @@ manifest_name="go.mod", executable_search_paths=None) +PROVIDED_MAKE_CONFIG = CONFIG( + language="provided", + dependency_manager=None, + application_framework=None, + manifest_name="Makefile", + executable_search_paths=None) + class UnsupportedRuntimeException(Exception): pass +class UnsupportedBuilderException(Exception): + pass + +def get_selector(selector_list, identifiers, specified_workflow=None): + """ + Determine the correct workflow selector from a list of selectors, series of identifiers and user specified workflow if defined. + + Parameters + ---------- + selector_list list + List of dictionaries, where the value of all dictionaries are workflow selectors. + identifiers list + List of identifiers specified in order of precedence that are to be looked up in selector_list. + specified_workflow str + User specified workflow for build. + + Returns + ------- + selector(BasicWorkflowSelector) + selector object which can specify a workflow configuration that can be passed to `aws-lambda-builders` + + """ + + # Create a combined view of all the selectors + all_selectors = {} + for selector in selector_list: + all_selectors = {**all_selectors, **selector} + + # Check for specified workflow being supported at all and if it's not, raise an UnsupportedBuilderException. + if specified_workflow and specified_workflow not in all_selectors: + raise UnsupportedBuilderException("'{}' does not have a supported builder".format(specified_workflow)) + + # Loop through all identifers to gather list of selectors with potential matches. + selectors = [all_selectors.get(identifier, None) for identifier in identifiers] + + # Intialize a `None` selector. + selector = None + + try: + # Find first non-None selector. + # Return the first selector with a match. + selector = next(_selector for _selector in selectors if _selector) + except StopIteration: + pass + + return selector def get_layer_subfolder(runtime): subfolders_by_runtime = { @@ -95,7 +148,7 @@ def get_layer_subfolder(runtime): return subfolders_by_runtime[runtime] -def get_workflow_config(runtime, code_dir, project_dir): +def get_workflow_config(runtime, code_dir, project_dir, specified_workflow=None): """ Get a workflow config that corresponds to the runtime provided. This method examines contents of the project and code directories to determine the most appropriate workflow for the given runtime. Currently the decision is @@ -113,25 +166,30 @@ def get_workflow_config(runtime, code_dir, project_dir): project_dir str Root of the Serverless application project. + specified_workflow str + Workflow to be used, if directly specified. They are currently scoped to "makefile" and the official runtime + identifier names themselves, eg: nodejs10.x. If a workflow is not directly specified, it is calculated by the current method + based on the runtime. + Returns ------- namedtuple(Capability) namedtuple that represents the Builder Workflow Config """ + selectors_by_build_method = { + "makefile": BasicWorkflowSelector(PROVIDED_MAKE_CONFIG) + } + selectors_by_runtime = { "python2.7": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "python3.6": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "python3.7": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "python3.8": BasicWorkflowSelector(PYTHON_PIP_CONFIG), - "nodejs4.3": BasicWorkflowSelector(NODEJS_NPM_CONFIG), - "nodejs6.10": BasicWorkflowSelector(NODEJS_NPM_CONFIG), - "nodejs8.10": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs10.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs12.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "ruby2.5": BasicWorkflowSelector(RUBY_BUNDLER_CONFIG), "ruby2.7": BasicWorkflowSelector(RUBY_BUNDLER_CONFIG), - "dotnetcore2.0": BasicWorkflowSelector(DOTNET_CLIPACKAGE_CONFIG), "dotnetcore2.1": BasicWorkflowSelector(DOTNET_CLIPACKAGE_CONFIG), "dotnetcore3.1": BasicWorkflowSelector(DOTNET_CLIPACKAGE_CONFIG), "go1.x": BasicWorkflowSelector(GO_MOD_CONFIG), @@ -150,21 +208,24 @@ def get_workflow_config(runtime, code_dir, project_dir): JAVA_KOTLIN_GRADLE_CONFIG._replace(executable_search_paths=[code_dir, project_dir]), JAVA_MAVEN_CONFIG ]), + "provided": BasicWorkflowSelector(PROVIDED_MAKE_CONFIG) } - + # First check if the runtime is present and is buildable, if not raise an UnsupportedRuntimeException Error. if runtime not in selectors_by_runtime: raise UnsupportedRuntimeException("'{}' runtime is not supported".format(runtime)) - selector = selectors_by_runtime[runtime] - try: + # Identify appropriate workflow selector. + selector = get_selector(selector_list=[selectors_by_build_method, selectors_by_runtime], identifiers=[specified_workflow, runtime], + specified_workflow=specified_workflow) + + # Identify workflow configuration from the workflow selector. config = selector.get_config(code_dir, project_dir) return config except ValueError as ex: raise UnsupportedRuntimeException("Unable to find a supported build workflow for runtime '{}'. Reason: {}" .format(runtime, str(ex))) - def supports_build_in_container(config): """ Given a workflow config, this method provides a boolean on whether the workflow can run within a container or not. diff --git a/samcli/lib/providers/provider.py b/samcli/lib/providers/provider.py index 631e70f0c5..dcb8936005 100644 --- a/samcli/lib/providers/provider.py +++ b/samcli/lib/providers/provider.py @@ -38,6 +38,8 @@ "layers", # Event "events", + # Metadata + "metadata", ], ) diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index 2f981c378e..71d50b9b02 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -112,6 +112,10 @@ def _extract_functions(resources, ignore_code_extraction_warnings=False): resource_type = resource.get("Type") resource_properties = resource.get("Properties", {}) + resource_metadata = resource.get("Metadata", None) + # Add extra metadata information to properties under a separate field. + if resource_metadata: + resource_properties["Metadata"] = resource_metadata if resource_type == SamFunctionProvider.SERVERLESS_FUNCTION: layers = SamFunctionProvider._parse_layer_info( @@ -224,6 +228,7 @@ def _build_function_configuration(name, codeuri, resource_properties, layers): rolearn=resource_properties.get("Role"), events=resource_properties.get("Events"), layers=layers, + metadata=resource_properties.get("Metadata", None), ) @staticmethod diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 7049ca97aa..1cbe9fe0b2 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -1,3 +1,4 @@ +import platform import sys import os import logging @@ -522,6 +523,7 @@ def test_must_fail_with_container(self, runtime, code_uri): self.assertEqual(process_execute.process.returncode, 1) 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)) @@ -831,3 +833,194 @@ def _verify_built_artifact( def _get_python_version(self): return "python{}.{}".format(sys.version_info.major, sys.version_info.minor) + + +class TestBuildCommand_ProvidedFunctions(BuildIntegBase): + # Test Suite for runtime: provided and where selection of the build workflow is implicitly makefile builder + # if the makefile is present. + + EXPECTED_FILES_GLOBAL_MANIFEST = set() + EXPECTED_FILES_PROJECT_MANIFEST = { + "__init__.py", + "main.py", + "requests", + "requirements.txt", + } + + FUNCTION_LOGICAL_ID = "Function" + + @parameterized.expand( + [("provided", False, None), ("provided", "use_container", "Makefile-container"),] + ) + @pytest.mark.flaky(reruns=3) + def test_with_Makefile(self, runtime, use_container, manifest): + overrides = {"Runtime": runtime, "CodeUri": "Provided", "Handler": "main.handler"} + manifest_path = None + if manifest: + manifest_path = os.path.join(self.test_data_path, "Provided", manifest) + + cmdlist = self.get_command_list( + use_container=use_container, parameter_overrides=overrides, manifest_path=manifest_path + ) + + LOG.info("Running Command: {}", cmdlist) + # Built using Makefile for a python project. + run_command(cmdlist, cwd=self.working_dir) + + self._verify_built_artifact( + self.default_build_dir, self.FUNCTION_LOGICAL_ID, self.EXPECTED_FILES_PROJECT_MANIFEST + ) + + expected = "2.23.0" + # Building was done with a makefile, but invoke should be checked with corresponding python image. + overrides["Runtime"] = self._get_python_version() + self._verify_invoke_built_function( + self.built_template, self.FUNCTION_LOGICAL_ID, 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.assertEqual(actual_files, expected_files) + + def _get_python_version(self): + return "python{}.{}".format(sys.version_info.major, sys.version_info.minor) + + +@skipIf( + ((IS_WINDOWS and RUNNING_ON_CI) and not CI_OVERRIDE), + "Skip build tests on windows when running in CI unless overridden", +) +class TestBuildWithBuildMethod(BuildIntegBase): + # Test Suite where `BuildMethod` is explicitly specified. + + template = "custom-build-function.yaml" + EXPECTED_FILES_GLOBAL_MANIFEST = set() + EXPECTED_FILES_PROJECT_MANIFEST = { + "__init__.py", + "main.py", + "requests", + "requirements.txt", + } + + FUNCTION_LOGICAL_ID = "Function" + + @parameterized.expand( + [(False, None, "makefile"), ("use_container", "Makefile-container", "makefile"),] + ) + @pytest.mark.flaky(reruns=3) + def test_with_makefile_builder_specified_python_runtime(self, use_container, manifest, build_method): + # runtime is chosen based off current python version. + runtime = self._get_python_version() + # Codeuri is still Provided, since that directory has the makefile. + overrides = {"Runtime": runtime, "CodeUri": "Provided", "Handler": "main.handler", "BuildMethod": build_method} + manifest_path = None + if manifest: + manifest_path = os.path.join(self.test_data_path, "Provided", manifest) + + cmdlist = self.get_command_list( + use_container=use_container, parameter_overrides=overrides, manifest_path=manifest_path + ) + + LOG.info("Running Command: {}", cmdlist) + # Built using Makefile for a python project. + run_command(cmdlist, cwd=self.working_dir) + + self._verify_built_artifact( + self.default_build_dir, self.FUNCTION_LOGICAL_ID, self.EXPECTED_FILES_PROJECT_MANIFEST + ) + + expected = "2.23.0" + # Building was done with a makefile, invoke is checked with the same runtime image. + self._verify_invoke_built_function( + self.built_template, self.FUNCTION_LOGICAL_ID, self._make_parameter_override_arg(overrides), expected + ) + self.verify_docker_container_cleanedup(runtime) + + @parameterized.expand( + [(False,), ("use_container"),] + ) + @pytest.mark.flaky(reruns=3) + def test_with_native_builder_specified_python_runtime(self, use_container): + # runtime is chosen based off current python version. + runtime = self._get_python_version() + # Codeuri is still Provided, since that directory has the makefile, but it also has the + # actual manifest file of `requirements.txt`. + # BuildMethod is set to the same name as of the runtime. + overrides = {"Runtime": runtime, "CodeUri": "Provided", "Handler": "main.handler", "BuildMethod": runtime} + manifest_path = os.path.join(self.test_data_path, "Provided", "requirements.txt") + + cmdlist = self.get_command_list( + use_container=use_container, parameter_overrides=overrides, manifest_path=manifest_path + ) + + LOG.info("Running Command: {}", cmdlist) + # Built using `native` python-pip builder for a python project. + run_command(cmdlist, cwd=self.working_dir) + + self._verify_built_artifact( + self.default_build_dir, self.FUNCTION_LOGICAL_ID, self.EXPECTED_FILES_PROJECT_MANIFEST + ) + + expected = "2.23.0" + # Building was done with a `python-pip` builder, invoke is checked with the same runtime image. + self._verify_invoke_built_function( + self.built_template, self.FUNCTION_LOGICAL_ID, self._make_parameter_override_arg(overrides), expected + ) + self.verify_docker_container_cleanedup(runtime) + + @parameterized.expand( + [(False,), ("use_container"),] + ) + @pytest.mark.flaky(reruns=3) + def test_with_wrong_builder_specified_python_runtime(self, use_container): + # runtime is chosen based off current python version. + runtime = self._get_python_version() + # BuildMethod is set to the ruby2.7, this should cause failure. + overrides = {"Runtime": runtime, "CodeUri": "Provided", "Handler": "main.handler", "BuildMethod": "ruby2.7"} + manifest_path = os.path.join(self.test_data_path, "Provided", "requirements.txt") + + cmdlist = self.get_command_list( + use_container=use_container, parameter_overrides=overrides, manifest_path=manifest_path + ) + + LOG.info("Running Command: {}", cmdlist) + # This will error out. + command = run_command(cmdlist, cwd=self.working_dir) + self.assertEqual(command.process.returncode, 1) + self.assertEqual(command.stdout.strip(), b"Build Failed") + + 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.assertEqual(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/testdata/buildcmd/Provided/Makefile b/tests/integration/testdata/buildcmd/Provided/Makefile new file mode 100644 index 0000000000..c6e094235a --- /dev/null +++ b/tests/integration/testdata/buildcmd/Provided/Makefile @@ -0,0 +1,5 @@ +build-Function: + cp *.py $(ARTIFACTS_DIR) + cp requirements.txt $(ARTIFACTS_DIR) + python -m pip install -r requirements.txt -t $(ARTIFACTS_DIR) + rm -rf $(ARTIFACTS_DIR)/bin \ No newline at end of file diff --git a/tests/integration/testdata/buildcmd/Provided/Makefile-container b/tests/integration/testdata/buildcmd/Provided/Makefile-container new file mode 100644 index 0000000000..eb9e17747b --- /dev/null +++ b/tests/integration/testdata/buildcmd/Provided/Makefile-container @@ -0,0 +1,7 @@ +build-Function: + cp *.py $(ARTIFACTS_DIR) + cp requirements.txt $(ARTIFACTS_DIR) + curl https://bootstrap.pypa.io/get-pip.py > /tmp/get-pip.py + python /tmp/get-pip.py + python -m pip install -r requirements.txt -t $(ARTIFACTS_DIR) + rm -rf $(ARTIFACTS_DIR)/bin \ No newline at end of file diff --git a/tests/integration/testdata/buildcmd/Provided/__init__.py b/tests/integration/testdata/buildcmd/Provided/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/buildcmd/Provided/main.py b/tests/integration/testdata/buildcmd/Provided/main.py new file mode 100644 index 0000000000..0c66c2b32f --- /dev/null +++ b/tests/integration/testdata/buildcmd/Provided/main.py @@ -0,0 +1,4 @@ +import requests + +def handler(event, context): + return requests.__version__ diff --git a/tests/integration/testdata/buildcmd/Provided/requirements.txt b/tests/integration/testdata/buildcmd/Provided/requirements.txt new file mode 100644 index 0000000000..822be7520a --- /dev/null +++ b/tests/integration/testdata/buildcmd/Provided/requirements.txt @@ -0,0 +1 @@ +requests==2.23.0 \ No newline at end of file diff --git a/tests/integration/testdata/buildcmd/custom-build-function.yaml b/tests/integration/testdata/buildcmd/custom-build-function.yaml new file mode 100644 index 0000000000..a17b6a5409 --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom-build-function.yaml @@ -0,0 +1,25 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Parameteres: + Runtime: + Type: String + CodeUri: + Type: String + Handler: + Type: String + BuildMethod: + Type: String + +Resources: + + Function: + Type: AWS::Serverless::Function + Properties: + Handler: !Ref Handler + Runtime: !Ref Runtime + CodeUri: !Ref CodeUri + Timeout: 600 + Metadata: + BuildMethod: !Ref BuildMethod + diff --git a/tests/unit/commands/local/lib/test_local_lambda.py b/tests/unit/commands/local/lib/test_local_lambda.py index 346fd7d43e..81e6d821c8 100644 --- a/tests/unit/commands/local/lib/test_local_lambda.py +++ b/tests/unit/commands/local/lib/test_local_lambda.py @@ -216,6 +216,7 @@ def test_must_work_with_override_values( rolearn=None, layers=[], events=None, + metadata=None, ) self.local_lambda.env_vars_values = env_vars_values @@ -257,6 +258,7 @@ def test_must_not_work_with_invalid_override_values(self, env_vars_values, expec rolearn=None, layers=[], events=None, + metadata=None, ) self.local_lambda.env_vars_values = env_vars_values @@ -289,6 +291,7 @@ def test_must_work_with_invalid_environment_variable(self, environment_variable, rolearn=None, layers=[], events=None, + metadata=None, ) self.local_lambda.env_vars_values = {} @@ -351,6 +354,7 @@ def test_must_work(self, FunctionConfigMock, is_debugging_mock, resolve_code_pat rolearn=None, layers=layers, events=None, + metadata=None, ) config = "someconfig" @@ -397,6 +401,7 @@ def test_timeout_set_to_max_during_debugging(self, FunctionConfigMock, is_debugg rolearn=None, layers=[], events=None, + metadata=None, ) config = "someconfig" diff --git a/tests/unit/commands/local/lib/test_sam_function_provider.py b/tests/unit/commands/local/lib/test_sam_function_provider.py index 48bf8a87f5..87324d6394 100644 --- a/tests/unit/commands/local/lib/test_sam_function_provider.py +++ b/tests/unit/commands/local/lib/test_sam_function_provider.py @@ -99,6 +99,7 @@ def setUp(self): rolearn=None, layers=[], events=None, + metadata=None, ), ), ( @@ -115,6 +116,7 @@ def setUp(self): rolearn=None, layers=[], events=None, + metadata=None, ), ), ( @@ -131,6 +133,7 @@ def setUp(self): rolearn=None, layers=[], events=None, + metadata=None, ), ), ( @@ -147,6 +150,7 @@ def setUp(self): rolearn=None, layers=[], events=None, + metadata=None, ), ), ( @@ -163,6 +167,7 @@ def setUp(self): rolearn=None, layers=[], events=None, + metadata=None, ), ), ( @@ -179,6 +184,7 @@ def setUp(self): rolearn=None, layers=[], events=None, + metadata=None, ), ), ( @@ -195,6 +201,7 @@ def setUp(self): rolearn=None, layers=[], events=None, + metadata=None, ), ), ( @@ -211,6 +218,7 @@ def setUp(self): rolearn=None, layers=[], events=None, + metadata=None, ), ), ] @@ -350,6 +358,7 @@ def test_must_convert(self): rolearn="myrole", layers=["Layer1", "Layer2"], events=None, + metadata=None, ) result = SamFunctionProvider._convert_sam_function_resource(name, properties, ["Layer1", "Layer2"]) @@ -373,6 +382,7 @@ def test_must_skip_non_existent_properties(self): rolearn=None, layers=[], events=None, + metadata=None, ) result = SamFunctionProvider._convert_sam_function_resource(name, properties, []) @@ -436,6 +446,7 @@ def test_must_convert(self): rolearn="myrole", layers=["Layer1", "Layer2"], events=None, + metadata=None, ) result = SamFunctionProvider._convert_lambda_function_resource(name, properties, ["Layer1", "Layer2"]) @@ -459,6 +470,7 @@ def test_must_skip_non_existent_properties(self): rolearn=None, layers=[], events=None, + metadata=None, ) result = SamFunctionProvider._convert_lambda_function_resource(name, properties, []) @@ -557,6 +569,7 @@ def test_must_return_function_value(self): rolearn=None, layers=[], events=None, + metadata=None, ) provider.functions = {"func1": function} diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index fcf5f10a64..15ccb9ec36 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -50,8 +50,8 @@ def test_must_iterate_on_functions_and_layers(self): build_function_mock.assert_has_calls( [ - call(self.func1.name, self.func1.codeuri, self.func1.runtime, self.func1.handler), - call(self.func2.name, self.func2.codeuri, self.func2.runtime, self.func2.handler), + call(self.func1.name, self.func1.codeuri, self.func1.runtime, self.func1.handler, self.func1.metadata), + call(self.func2.name, self.func2.codeuri, self.func2.runtime, self.func2.handler, self.func2.metadata), ], any_order=False, ) @@ -129,6 +129,36 @@ def test_must_build_in_process(self, osutils_mock, get_workflow_config_mock): config_mock, code_dir, artifacts_dir, scratch_dir, manifest_path, runtime, None ) + @patch("samcli.lib.build.app_builder.get_workflow_config") + @patch("samcli.lib.build.app_builder.osutils") + def test_must_build_in_process_with_metadata(self, osutils_mock, get_workflow_config_mock): + function_name = "function_name" + codeuri = "path/to/source" + runtime = "runtime" + scratch_dir = "scratch" + handler = "handler.handle" + config_mock = get_workflow_config_mock.return_value = Mock() + config_mock.manifest_name = "manifest_name" + + osutils_mock.mkdir_temp.return_value.__enter__ = Mock(return_value=scratch_dir) + osutils_mock.mkdir_temp.return_value.__exit__ = Mock() + + self.builder._build_function_in_process = Mock() + + code_dir = str(Path("/base/dir/path/to/source").resolve()) + artifacts_dir = str(Path("/build/dir/function_name")) + manifest_path = str(Path(os.path.join(code_dir, config_mock.manifest_name)).resolve()) + + self.builder._build_function(function_name, codeuri, runtime, handler, metadata={"BuildMethod": "Workflow"}) + + get_workflow_config_mock.assert_called_with( + runtime, code_dir, self.builder._base_dir, specified_workflow="Workflow" + ) + + self.builder._build_function_in_process.assert_called_with( + config_mock, code_dir, artifacts_dir, scratch_dir, manifest_path, runtime, None + ) + @patch("samcli.lib.build.app_builder.get_workflow_config") @patch("samcli.lib.build.app_builder.osutils") def test_must_build_in_container(self, osutils_mock, get_workflow_config_mock): diff --git a/tests/unit/lib/build_module/test_workflow_config.py b/tests/unit/lib/build_module/test_workflow_config.py index eab6393b7a..3c4247a1d9 100644 --- a/tests/unit/lib/build_module/test_workflow_config.py +++ b/tests/unit/lib/build_module/test_workflow_config.py @@ -2,7 +2,11 @@ from parameterized import parameterized from unittest.mock import patch -from samcli.lib.build.workflow_config import get_workflow_config, UnsupportedRuntimeException +from samcli.lib.build.workflow_config import ( + get_workflow_config, + UnsupportedRuntimeException, + UnsupportedBuilderException, +) class Test_get_workflow_config(TestCase): @@ -10,7 +14,7 @@ def setUp(self): self.code_dir = "" self.project_dir = "" - @parameterized.expand([("python2.7",), ("python3.6",)]) + @parameterized.expand([("python2.7",), ("python3.6",), ("python3.7",), ("python3.8",)]) def test_must_work_for_python(self, runtime): result = get_workflow_config(runtime, self.code_dir, self.project_dir) @@ -20,7 +24,7 @@ def test_must_work_for_python(self, runtime): self.assertEqual(result.manifest_name, "requirements.txt") self.assertIsNone(result.executable_search_paths) - @parameterized.expand([("nodejs4.3",), ("nodejs6.10",), ("nodejs8.10",)]) + @parameterized.expand([("nodejs10.x",), ("nodejs12.x",)]) def test_must_work_for_nodejs(self, runtime): result = get_workflow_config(runtime, self.code_dir, self.project_dir) @@ -30,7 +34,31 @@ def test_must_work_for_nodejs(self, runtime): self.assertEqual(result.manifest_name, "package.json") self.assertIsNone(result.executable_search_paths) - @parameterized.expand([("ruby2.5",)]) + @parameterized.expand([("provided",)]) + def test_must_work_for_provided(self, runtime): + result = get_workflow_config(runtime, self.code_dir, self.project_dir, specified_workflow="makefile") + self.assertEqual(result.language, "provided") + self.assertEqual(result.dependency_manager, None) + self.assertEqual(result.application_framework, None) + self.assertEqual(result.manifest_name, "Makefile") + self.assertIsNone(result.executable_search_paths) + + @parameterized.expand([("provided",)]) + def test_must_work_for_provided_with_no_specified_workflow(self, runtime): + # Implicitly look for makefile capability. + result = get_workflow_config(runtime, self.code_dir, self.project_dir) + self.assertEqual(result.language, "provided") + self.assertEqual(result.dependency_manager, None) + self.assertEqual(result.application_framework, None) + self.assertEqual(result.manifest_name, "Makefile") + self.assertIsNone(result.executable_search_paths) + + @parameterized.expand([("provided",)]) + def test_raise_exception_for_bad_specified_workflow(self, runtime): + with self.assertRaises(UnsupportedBuilderException): + get_workflow_config(runtime, self.code_dir, self.project_dir, specified_workflow="Wrong") + + @parameterized.expand([("ruby2.5",), ("ruby2.7",)]) def test_must_work_for_ruby(self, runtime): result = get_workflow_config(runtime, self.code_dir, self.project_dir) self.assertEqual(result.language, "ruby") From 39add9acd0c7532129a6543134919eb6ae9df4f3 Mon Sep 17 00:00:00 2001 From: Tarun Date: Wed, 20 May 2020 16:27:04 -0700 Subject: [PATCH 2/3] feat: adding makefile workflow for layers build. (#2000) * Enabling makefile workflow for layers build. * Adding integration tests for build for layers with makefile. * Removing dangerous default value. * Fixing unit tests. * Address PR commetns. --- samcli/lib/build/app_builder.py | 22 +++++++++++------ samcli/lib/build/workflow_config.py | 14 +++++++---- samcli/lib/providers/provider.py | 10 ++++++-- samcli/lib/providers/sam_function_provider.py | 5 +++- samcli/lib/providers/sam_layer_provider.py | 3 ++- tests/integration/buildcmd/test_build_cmd.py | 24 +++++++++++++++++++ .../testdata/buildcmd/PyLayerMake/Makefile | 5 ++++ .../testdata/buildcmd/PyLayerMake/__init__.py | 0 .../testdata/buildcmd/PyLayerMake/layer.py | 5 ++++ .../buildcmd/PyLayerMake/requirements.txt | 6 +++++ .../buildcmd/layers-functions-template.yaml | 12 ++++++++++ .../unit/commands/local/lib/test_provider.py | 2 +- .../local/lib/test_sam_layer_provider.py | 24 ++++++++++++++----- 13 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 tests/integration/testdata/buildcmd/PyLayerMake/Makefile create mode 100644 tests/integration/testdata/buildcmd/PyLayerMake/__init__.py create mode 100644 tests/integration/testdata/buildcmd/PyLayerMake/layer.py create mode 100644 tests/integration/testdata/buildcmd/PyLayerMake/requirements.txt diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index e1f9d9c903..1d78aacf14 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -122,7 +122,8 @@ def build(self): f"Layer {layer.name} cannot be build without BuildMethod. Please provide BuildMethod in Metadata.") result[layer.name] = self._build_layer(layer.name, layer.codeuri, - layer.build_method) + layer.build_method, + layer.compatible_runtimes) return result @@ -172,13 +173,13 @@ def update_template(self, template_dict, original_template_path, built_artifacts return template_dict - def _build_layer(self, layer_name, codeuri, runtime): + def _build_layer(self, layer_name, codeuri, specified_workflow, compatible_runtimes): # Create the arguments to pass to the builder # Code is always relative to the given base directory. code_dir = str(pathlib.Path(self._base_dir, codeuri).resolve()) - config = get_workflow_config(runtime, code_dir, self._base_dir) - subfolder = get_layer_subfolder(runtime) + config = get_workflow_config(None, code_dir, self._base_dir, specified_workflow) + subfolder = get_layer_subfolder(specified_workflow) # artifacts directory will be created by the builder artifacts_dir = str(pathlib.Path(self._build_dir, layer_name, subfolder)) @@ -187,17 +188,24 @@ def _build_layer(self, layer_name, codeuri, runtime): manifest_path = self._manifest_path_override or os.path.join(code_dir, config.manifest_name) # By default prefer to build in-process for speed + build_runtime = specified_workflow build_method = self._build_function_in_process if self._container_manager: - build_method = self._build_function_on_container + build_method = self._build_function_in_process + if config.language == "provided": + LOG.warning( + "For container layer build, first compatible runtime is chosen as build target for container.") + # Only set to this value if specified workflow is makefile which will result in config language as provided + build_runtime = compatible_runtimes[0] + options = ApplicationBuilder._get_build_options(layer_name, config.language, None) build_method(config, code_dir, artifacts_dir, scratch_dir, manifest_path, - runtime, - None) + build_runtime, + options) # Not including subfolder in return so that we copy subfolder, instead of copying artifacts inside it. return str(pathlib.Path(self._build_dir, layer_name)) diff --git a/samcli/lib/build/workflow_config.py b/samcli/lib/build/workflow_config.py index 2d6d0335cf..6ddae245b7 100644 --- a/samcli/lib/build/workflow_config.py +++ b/samcli/lib/build/workflow_config.py @@ -125,7 +125,7 @@ def get_selector(selector_list, identifiers, specified_workflow=None): return selector -def get_layer_subfolder(runtime): +def get_layer_subfolder(build_workflow): subfolders_by_runtime = { "python2.7": "python", "python3.6": "python", @@ -140,12 +140,14 @@ def get_layer_subfolder(runtime): "ruby2.7": "ruby/lib", "java8": "java", "java11": "java", + # User is responsible for creating subfolder in these workflows + "makefile": "", } - if runtime not in subfolders_by_runtime: - raise UnsupportedRuntimeException("'{}' runtime is not supported for layers".format(runtime)) + if build_workflow not in subfolders_by_runtime: + raise UnsupportedRuntimeException("'{}' runtime is not supported for layers".format(build_workflow)) - return subfolders_by_runtime[runtime] + return subfolders_by_runtime[build_workflow] def get_workflow_config(runtime, code_dir, project_dir, specified_workflow=None): @@ -211,7 +213,9 @@ def get_workflow_config(runtime, code_dir, project_dir, specified_workflow=None) "provided": BasicWorkflowSelector(PROVIDED_MAKE_CONFIG) } # First check if the runtime is present and is buildable, if not raise an UnsupportedRuntimeException Error. - if runtime not in selectors_by_runtime: + # If runtime is present it should be in selectors_by_runtime, however for layers there will be no runtime so in that case + # we move ahead and resolve to any matching workflow from both types. + if runtime and runtime not in selectors_by_runtime: raise UnsupportedRuntimeException("'{}' runtime is not supported".format(runtime)) try: diff --git a/samcli/lib/providers/provider.py b/samcli/lib/providers/provider.py index dcb8936005..2b0e2c2b92 100644 --- a/samcli/lib/providers/provider.py +++ b/samcli/lib/providers/provider.py @@ -82,7 +82,7 @@ class LayerVersion: LAYER_NAME_DELIMETER = "-" - def __init__(self, arn, codeuri, metadata=None): + def __init__(self, arn, codeuri, compatible_runtimes=None, metadata=None): """ Parameters ---------- @@ -91,6 +91,8 @@ def __init__(self, arn, codeuri, metadata=None): codeuri str CodeURI of the layer. This should contain the path to the layer code """ + if compatible_runtimes is None: + compatible_runtimes = [] if metadata is None: metadata = {} if not isinstance(arn, str): @@ -102,6 +104,7 @@ def __init__(self, arn, codeuri, metadata=None): self._name = LayerVersion._compute_layer_name(self.is_defined_within_template, arn) self._version = LayerVersion._compute_layer_version(self.is_defined_within_template, arn) self._build_method = metadata.get("BuildMethod", None) + self._compatible_runtimes = compatible_runtimes @staticmethod def _compute_layer_version(is_defined_within_template, arn): @@ -208,10 +211,13 @@ def codeuri(self, codeuri): def build_method(self): return self._build_method + @property + def compatible_runtimes(self): + return self._compatible_runtimes + def __eq__(self, other): if isinstance(other, type(self)): return self.__dict__ == other.__dict__ - return False diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index 71d50b9b02..39ae38e13d 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -281,6 +281,7 @@ def _parse_layer_info(list_of_layers, resources, ignore_code_extraction_warnings layer_properties = layer_resource.get("Properties", {}) resource_type = layer_resource.get("Type") + compatible_runtimes = layer_properties.get("CompatibleRuntimes") codeuri = None if resource_type == SamFunctionProvider.LAMBDA_LAYER: @@ -291,6 +292,8 @@ def _parse_layer_info(list_of_layers, resources, ignore_code_extraction_warnings layer_logical_id, layer_properties, "ContentUri", ignore_code_extraction_warnings ) - layers.append(LayerVersion(layer_logical_id, codeuri, layer_resource.get("Metadata", None))) + layers.append( + LayerVersion(layer_logical_id, codeuri, compatible_runtimes, layer_resource.get("Metadata", None)) + ) return layers diff --git a/samcli/lib/providers/sam_layer_provider.py b/samcli/lib/providers/sam_layer_provider.py index b5a440219b..703cf8a659 100644 --- a/samcli/lib/providers/sam_layer_provider.py +++ b/samcli/lib/providers/sam_layer_provider.py @@ -91,6 +91,7 @@ def _convert_lambda_layer_resource(self, layer_logical_id, layer_resource): # When running locally, we need to follow that Ref so we can extract the local path to the layer code. layer_properties = layer_resource.get("Properties", {}) resource_type = layer_resource.get("Type") + compatible_runtimes = layer_properties.get("CompatibleRuntimes") codeuri = None if resource_type == self.SERVERLESS_LAYER: @@ -98,4 +99,4 @@ def _convert_lambda_layer_resource(self, layer_logical_id, layer_resource): if resource_type == self.LAMBDA_LAYER: codeuri = SamLayerProvider._extract_lambda_function_code(layer_properties, "Content") - return LayerVersion(layer_logical_id, codeuri, layer_resource.get("Metadata", None)) + return LayerVersion(layer_logical_id, codeuri, compatible_runtimes, layer_resource.get("Metadata", None)) diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 1cbe9fe0b2..4c3bcc2974 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -736,6 +736,29 @@ def test_build_single_layer(self, runtime, use_container, layer_identifier): "python", ) + @parameterized.expand( + [("makefile", False, "LayerWithMakefile"), ("makefile", "use_container", "LayerWithMakefile")] + ) + def test_build_layer_with_makefile(self, build_method, use_container, layer_identifier): + overrides = {"LayerBuildMethod": build_method, "LayerMakeContentUri": "PyLayerMake"} + cmdlist = self.get_command_list( + use_container=use_container, parameter_overrides=overrides, function_identifier=layer_identifier + ) + + LOG.info("Running Command:") + LOG.info(cmdlist) + + run_command(cmdlist, cwd=self.working_dir) + + LOG.info("Default build dir: %s", self.default_build_dir) + self._verify_built_artifact( + self.default_build_dir, + layer_identifier, + self.EXPECTED_LAYERS_FILES_PROJECT_MANIFEST, + "ContentUri", + "python", + ) + @parameterized.expand([("python3.7", False, "LayerTwo"), ("python3.7", "use_container", "LayerTwo")]) def test_build_fails_with_missing_metadata(self, runtime, use_container, layer_identifier): overrides = {"LayerBuildMethod": runtime, "LayerContentUri": "PyLayer"} @@ -755,6 +778,7 @@ def test_build_function_and_layer(self, runtime, use_container): overrides = { "LayerBuildMethod": runtime, "LayerContentUri": "PyLayer", + "LayerMakeContentUri": "PyLayerMake", "Runtime": runtime, "CodeUri": "PythonWithLayer", "Handler": "main.handler", diff --git a/tests/integration/testdata/buildcmd/PyLayerMake/Makefile b/tests/integration/testdata/buildcmd/PyLayerMake/Makefile new file mode 100644 index 0000000000..d33129d4c9 --- /dev/null +++ b/tests/integration/testdata/buildcmd/PyLayerMake/Makefile @@ -0,0 +1,5 @@ +build-LayerWithMakefile: + mkdir -p "$(ARTIFACTS_DIR)/python" + cp *.py "$(ARTIFACTS_DIR)/python" + cp *.txt "$(ARTIFACTS_DIR)/python" + python -m pip install -r requirements.txt -t "$(ARTIFACTS_DIR)/python" diff --git a/tests/integration/testdata/buildcmd/PyLayerMake/__init__.py b/tests/integration/testdata/buildcmd/PyLayerMake/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/buildcmd/PyLayerMake/layer.py b/tests/integration/testdata/buildcmd/PyLayerMake/layer.py new file mode 100644 index 0000000000..543c4f51f1 --- /dev/null +++ b/tests/integration/testdata/buildcmd/PyLayerMake/layer.py @@ -0,0 +1,5 @@ +import numpy + + +def layer_method(): + return {"pi": "{0:.2f}".format(numpy.pi)} diff --git a/tests/integration/testdata/buildcmd/PyLayerMake/requirements.txt b/tests/integration/testdata/buildcmd/PyLayerMake/requirements.txt new file mode 100644 index 0000000000..bf8549f936 --- /dev/null +++ b/tests/integration/testdata/buildcmd/PyLayerMake/requirements.txt @@ -0,0 +1,6 @@ +# These are some hard packages to build. Using them here helps us verify that building works on various platforms + +numpy~=1.15 +# `cryptography` has a dependency on `pycparser` which, for some reason doesn't build inside a Docker container. +# Turning this off until we resolve this issue: https://github.com/awslabs/aws-lambda-builders/issues/29 +# cryptography~=2.4 diff --git a/tests/integration/testdata/buildcmd/layers-functions-template.yaml b/tests/integration/testdata/buildcmd/layers-functions-template.yaml index 7df0cb43f1..1499628d74 100644 --- a/tests/integration/testdata/buildcmd/layers-functions-template.yaml +++ b/tests/integration/testdata/buildcmd/layers-functions-template.yaml @@ -10,6 +10,8 @@ Parameteres: Type: String LayerContentUri: Type: String + LayerMakeContentUri: + Type: String LayerBuildMethod: Type: String @@ -42,3 +44,13 @@ Resources: ContentUri: !Ref LayerContentUri CompatibleRuntimes: - python3.7 + + LayerWithMakefile: + Type: AWS::Serverless::LayerVersion + Properties: + Description: Layer three + ContentUri: !Ref LayerMakeContentUri + CompatibleRuntimes: + - python3.7 + Metadata: + BuildMethod: !Ref LayerBuildMethod diff --git a/tests/unit/commands/local/lib/test_provider.py b/tests/unit/commands/local/lib/test_provider.py index 47c1a59da1..aa74707f1e 100644 --- a/tests/unit/commands/local/lib/test_provider.py +++ b/tests/unit/commands/local/lib/test_provider.py @@ -30,7 +30,7 @@ def test_layer_arn_returned(self): def test_layer_build_method_returned(self): layer_version = LayerVersion( - "arn:aws:lambda:region:account-id:layer:layer-name:1", None, {"BuildMethod": "dummy_build_method"} + "arn:aws:lambda:region:account-id:layer:layer-name:1", None, [], {"BuildMethod": "dummy_build_method"} ) self.assertEqual(layer_version.build_method, "dummy_build_method") diff --git a/tests/unit/commands/local/lib/test_sam_layer_provider.py b/tests/unit/commands/local/lib/test_sam_layer_provider.py index 2b5f26c4f9..4702238a7c 100644 --- a/tests/unit/commands/local/lib/test_sam_layer_provider.py +++ b/tests/unit/commands/local/lib/test_sam_layer_provider.py @@ -77,12 +77,24 @@ def setUp(self): @parameterized.expand( [ - ("ServerlessLayer", LayerVersion("ServerlessLayer", "PyLayer/", {"BuildMethod": "python3.8"})), - ("LambdaLayer", LayerVersion("LambdaLayer", "PyLayer/", {"BuildMethod": "python3.8"})), - ("ServerlessLayerNoBuild", LayerVersion("ServerlessLayerNoBuild", "PyLayer/", None)), - ("LambdaLayerNoBuild", LayerVersion("LambdaLayerNoBuild", "PyLayer/", None)), - ("ServerlessLayerS3Content", LayerVersion("ServerlessLayerS3Content", ".", None)), - ("LambdaLayerS3Content", LayerVersion("LambdaLayerS3Content", ".", None)), + ( + "ServerlessLayer", + LayerVersion("ServerlessLayer", "PyLayer/", ["python3.8", "python3.6"], {"BuildMethod": "python3.8"}), + ), + ( + "LambdaLayer", + LayerVersion("LambdaLayer", "PyLayer/", ["python3.8", "python3.6"], {"BuildMethod": "python3.8"}), + ), + ( + "ServerlessLayerNoBuild", + LayerVersion("ServerlessLayerNoBuild", "PyLayer/", ["python3.8", "python3.6"], None), + ), + ("LambdaLayerNoBuild", LayerVersion("LambdaLayerNoBuild", "PyLayer/", ["python3.8", "python3.6"], None)), + ( + "ServerlessLayerS3Content", + LayerVersion("ServerlessLayerS3Content", ".", ["python3.8", "python3.6"], None), + ), + ("LambdaLayerS3Content", LayerVersion("LambdaLayerS3Content", ".", ["python3.8", "python3.6"], None)), ] ) def test_get_must_return_each_layer(self, name, expected_output): From 98be672418f76bf1cc54db7528988b8a1c6a6b52 Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan <3770774+sriram-mv@users.noreply.github.com> Date: Thu, 21 May 2020 10:29:26 -0700 Subject: [PATCH 3/3] chore: version v0.51.0 (#2004) --- samcli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samcli/__init__.py b/samcli/__init__.py index 9ac1025aea..0b83ca18c8 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = "0.50.0" +__version__ = "0.51.0"