diff --git a/.pylintrc b/.pylintrc index c0987c591e..1d864729c6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,7 +9,7 @@ # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=compat.py +ignore=compat.py, __main__.py # Pickle collected data for later comparisons. persistent=yes diff --git a/.travis.yml b/.travis.yml index b729efac2d..91fc368026 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ # Enable container based builds sudo: required language: python +dist: xenial services: - docker @@ -8,15 +9,19 @@ services: python: - "2.7" - "3.6" + - "3.7" -# Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs -matrix: - include: - - python: 3.7 - dist: xenial - sudo: true +addons: + apt: + packages: + # Xenial images don't have jdk8 installed by default. + - openjdk-8-jdk before_install: + # Use the JDK8 that we installed + - JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-amd64 + - PATH=$JAVA_HOME/bin:$PATH + - nvm install 8.10 - npm --version - node --version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6969174dfc..0b97a3babb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ information to effectively respond to your bug report or contribution. ## Development Guide -Refer to the [Development Guide](DEVELOPMENT_GUIDE.rst) for help with environment setup, running tests, submitting a PR, or anything that will make you more productive. +Refer to the [Development Guide](DEVELOPMENT_GUIDE.md) for help with environment setup, running tests, submitting a PR, or anything that will make you more productive. ## Reporting Bugs/Feature Requests diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md index 7b9117b25f..0934f2d785 100644 --- a/DEVELOPMENT_GUIDE.md +++ b/DEVELOPMENT_GUIDE.md @@ -156,7 +156,7 @@ Design Document A design document is a written description of the feature/capability you are building. We have a [design document -template](./designs/_template.rst) to help you quickly fill in the +template](./designs/_template.md) to help you quickly fill in the blanks and get you working quickly. We encourage you to write a design document for any feature you write, but for some types of features we definitely require a design document to proceed with implementation. diff --git a/requirements/base.txt b/requirements/base.txt index d947ecc626..65bbf5ee59 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,11 +6,11 @@ Flask~=1.0.2 boto3~=1.9, >=1.9.56 PyYAML~=3.12 cookiecutter~=1.6.0 -aws-sam-translator==1.9.0 +aws-sam-translator==1.9.1 docker>=3.3.0 dateparser~=0.7 python-dateutil~=2.6 pathlib2~=2.3.2; python_version<"3.4" requests==2.20.1 -aws_lambda_builders==0.0.5 +aws_lambda_builders==0.1.0 serverlessrepo==0.1.5 diff --git a/samcli/__init__.py b/samcli/__init__.py index fb9abb1b79..dae37706fb 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = '0.11.0' +__version__ = '0.12.0' diff --git a/samcli/__main__.py b/samcli/__main__.py new file mode 100644 index 0000000000..89a662cc32 --- /dev/null +++ b/samcli/__main__.py @@ -0,0 +1,12 @@ +""" +Invokable Module for CLI + +python -m samcli +""" + +from samcli.cli.main import cli + +if __name__ == "__main__": + # NOTE(TheSriram): prog_name is always set to "sam". This way when the CLI is invoked as a module, + # the help text that is generated still says "sam" instead of "__main__". + cli(prog_name="sam") diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index 0911732627..a8c3660eb3 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -11,8 +11,8 @@ from samcli.commands._utils.options import template_option_without_build, docker_common_options, \ parameter_override_option from samcli.commands.build.build_context import BuildContext -from samcli.lib.build.app_builder import ApplicationBuilder, UnsupportedRuntimeException, \ - BuildError, UnsupportedBuilderLibraryVersionError +from samcli.lib.build.app_builder import ApplicationBuilder, BuildError, UnsupportedBuilderLibraryVersionError +from samcli.lib.build.workflow_config import UnsupportedRuntimeException from samcli.commands._utils.template import move_template LOG = logging.getLogger(__name__) @@ -30,9 +30,10 @@ \b Supported Runtimes ------------------ -1. Python2.7\n -2. Python3.6\n -3. Python3.7\n +1. Python 2.7, 3.6, 3.7 using PIP\n +4. Nodejs 8.10, 6.10 using NPM +4. Ruby 2.5 using Bundler +5. Java 8 using Gradle \b Examples -------- diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index 689835ec6e..0e0ac5a93a 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -6,7 +6,6 @@ import io import json import logging -from collections import namedtuple try: import pathlib @@ -20,15 +19,12 @@ from aws_lambda_builders.builder import LambdaBuilder from aws_lambda_builders.exceptions import LambdaBuilderError from aws_lambda_builders import RPC_PROTOCOL_VERSION as lambda_builders_protocol_version +from .workflow_config import get_workflow_config LOG = logging.getLogger(__name__) -class UnsupportedRuntimeException(Exception): - pass - - class UnsupportedBuilderLibraryVersionError(Exception): def __init__(self, container_name, error_msg): @@ -41,32 +37,6 @@ class BuildError(Exception): pass -def _get_workflow_config(runtime): - - config = namedtuple('Capability', ["language", "dependency_manager", "application_framework", "manifest_name"]) - - if runtime.startswith("python"): - return config( - language="python", - dependency_manager="pip", - application_framework=None, - manifest_name="requirements.txt") - elif runtime.startswith("nodejs"): - return config( - language="nodejs", - dependency_manager="npm", - application_framework=None, - manifest_name="package.json") - elif runtime.startswith("ruby"): - return config( - language="ruby", - dependency_manager="bundler", - application_framework=None, - manifest_name="Gemfile") - else: - raise UnsupportedRuntimeException("'{}' runtime is not supported".format(runtime)) - - class ApplicationBuilder(object): """ Class to build an entire application. Currently, this class builds Lambda functions only, but there is nothing that @@ -174,14 +144,33 @@ def update_template(self, template_dict, original_template_path, built_artifacts return template_dict def _build_function(self, function_name, codeuri, runtime): + """ + 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. + + Parameters + ---------- + function_name : str + Name or LogicalId of the function - config = _get_workflow_config(runtime) + codeuri : str + Path to where the code lives - # Create the arguments to pass to the builder + runtime : str + AWS Lambda function runtime + + Returns + ------- + str + Path to the location where built artifacts are available + """ + # 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) + # artifacts directory will be created by the builder artifacts_dir = str(pathlib.Path(self._build_dir, function_name)) @@ -217,7 +206,8 @@ def _build_function_in_process(self, artifacts_dir, scratch_dir, manifest_path, - runtime=runtime) + runtime=runtime, + executable_search_paths=config.executable_search_paths) except LambdaBuilderError as ex: raise BuildError(str(ex)) @@ -243,7 +233,8 @@ def _build_function_on_container(self, # pylint: disable=too-many-locals runtime, log_level=log_level, optimizations=None, - options=None) + options=None, + executable_search_paths=config.executable_search_paths) try: try: diff --git a/samcli/lib/build/workflow_config.py b/samcli/lib/build/workflow_config.py new file mode 100644 index 0000000000..055b0c2240 --- /dev/null +++ b/samcli/lib/build/workflow_config.py @@ -0,0 +1,159 @@ +""" +Contains Builder Workflow Configs for different Runtimes +""" + +import os +import logging +from collections import namedtuple + + +LOG = logging.getLogger(__name__) + + +CONFIG = namedtuple('Capability', ["language", "dependency_manager", "application_framework", "manifest_name", + "executable_search_paths"]) + +PYTHON_PIP_CONFIG = CONFIG( + language="python", + dependency_manager="pip", + application_framework=None, + manifest_name="requirements.txt", + executable_search_paths=None) + +NODEJS_NPM_CONFIG = CONFIG( + language="nodejs", + dependency_manager="npm", + application_framework=None, + manifest_name="package.json", + executable_search_paths=None) + +RUBY_BUNDLER_CONFIG = CONFIG( + language="ruby", + dependency_manager="bundler", + application_framework=None, + manifest_name="Gemfile", + executable_search_paths=None) + +JAVA_GRADLE_CONFIG = CONFIG( + language="java", + dependency_manager="gradle", + application_framework=None, + manifest_name="build.gradle", + executable_search_paths=None) + + +class UnsupportedRuntimeException(Exception): + pass + + +def get_workflow_config(runtime, code_dir, project_dir): + """ + 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 + based on the presence of a supported manifest file. For runtimes that have more than one workflow, we choose a + workflow by examining ``code_dir`` followed by ``project_dir`` for presence of a supported manifest. + + Parameters + ---------- + runtime str + The runtime of the config + + code_dir str + Directory where Lambda function code is present + + project_dir str + Root of the Serverless application project. + + Returns + ------- + namedtuple(Capability) + namedtuple that represents the Builder Workflow Config + """ + + selectors_by_runtime = { + "python2.7": BasicWorkflowSelector(PYTHON_PIP_CONFIG), + "python3.6": BasicWorkflowSelector(PYTHON_PIP_CONFIG), + "python3.7": BasicWorkflowSelector(PYTHON_PIP_CONFIG), + "nodejs4.3": BasicWorkflowSelector(NODEJS_NPM_CONFIG), + "nodejs6.10": BasicWorkflowSelector(NODEJS_NPM_CONFIG), + "nodejs8.10": BasicWorkflowSelector(NODEJS_NPM_CONFIG), + "ruby2.5": BasicWorkflowSelector(RUBY_BUNDLER_CONFIG), + + # When Maven builder exists, add to this list so we can automatically choose a builder based on the supported + # manifest + "java8": ManifestWorkflowSelector([ + # Gradle builder needs custom executable paths to find `gradlew` binary + JAVA_GRADLE_CONFIG._replace(executable_search_paths=[code_dir, project_dir]) + ]), + } + + if runtime not in selectors_by_runtime: + raise UnsupportedRuntimeException("'{}' runtime is not supported".format(runtime)) + + selector = selectors_by_runtime[runtime] + + try: + 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))) + + +class BasicWorkflowSelector(object): + """ + Basic workflow selector that returns the first available configuration in the given list of configurations + """ + + def __init__(self, configs): + + if not isinstance(configs, list): + configs = [configs] + + self.configs = configs + + def get_config(self, code_dir, project_dir): + """ + Returns the first available configuration + """ + return self.configs[0] + + +class ManifestWorkflowSelector(BasicWorkflowSelector): + """ + Selects a workflow by examining the directories for presence of a supported manifest + """ + + def get_config(self, code_dir, project_dir): + """ + Finds a configuration by looking for a manifest in the given directories. + + Returns + ------- + samcli.lib.build.workflow_config.CONFIG + A supported configuration if one is found + + Raises + ------ + ValueError + If none of the supported manifests files are found + """ + + # Search for manifest first in code directory and then in the project directory. + # Search order is important here because we want to prefer the manifest present within the code directory over + # a manifest present in project directory. + search_dirs = [code_dir, project_dir] + LOG.debug("Looking for a supported build workflow in following directories: %s", search_dirs) + + for config in self.configs: + + if any([self._has_manifest(config, directory) for directory in search_dirs]): + return config + + raise ValueError("None of the supported manifests '{}' were found in the following paths '{}'".format( + [config.manifest_name for config in self.configs], + search_dirs)) + + @staticmethod + def _has_manifest(config, directory): + return os.path.exists(os.path.join(directory, config.manifest_name)) diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index fb71e0ed6a..e1d2a9114a 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -211,7 +211,11 @@ def _parse_lambda_output(lambda_output, binary_types, flask_request): body = json_output.get("body") or "no data" is_base_64_encoded = json_output.get("isBase64Encoded") or False - if not isinstance(status_code, int) or status_code <= 0: + try: + status_code = int(status_code) + if status_code <= 0: + raise ValueError + except ValueError: message = "statusCode must be a positive int" LOG.error(message) raise TypeError(message) diff --git a/samcli/local/docker/lambda_build_container.py b/samcli/local/docker/lambda_build_container.py index 368c9b054d..b7ce5ff60d 100644 --- a/samcli/local/docker/lambda_build_container.py +++ b/samcli/local/docker/lambda_build_container.py @@ -35,6 +35,7 @@ def __init__(self, # pylint: disable=too-many-locals runtime, optimizations=None, options=None, + executable_search_paths=None, log_level=None): abs_manifest_path = pathlib.Path(manifest_path).resolve() @@ -45,6 +46,18 @@ def __init__(self, # pylint: disable=too-many-locals container_dirs = LambdaBuildContainer._get_container_dirs(source_dir, manifest_dir) + # `executable_search_paths` are provided as a list of paths on the host file system that needs to passed to + # the builder. But these paths don't exist within the container. We use the following method to convert the + # host paths to container paths. But if a host path is NOT mounted within the container, we will simply ignore + # it. In essence, only when the path is already in the mounted path, can the path resolver within the + # container even find the executable. + executable_search_paths = LambdaBuildContainer._convert_to_container_dirs( + host_paths_to_convert=executable_search_paths, + host_to_container_path_mapping={ + source_dir: container_dirs["source_dir"], + manifest_dir: container_dirs["manifest_dir"] + }) + request_json = self._make_request(protocol_version, language, dependency_manager, @@ -53,7 +66,8 @@ def __init__(self, # pylint: disable=too-many-locals manifest_file_name, runtime, optimizations, - options) + options, + executable_search_paths) image = LambdaBuildContainer._get_image(runtime) entry = LambdaBuildContainer._get_entrypoint(request_json) @@ -96,7 +110,8 @@ def _make_request(protocol_version, manifest_file_name, runtime, optimizations, - options): + options, + executable_search_paths): return json.dumps({ "jsonschema": "2.0", @@ -119,6 +134,7 @@ def _make_request(protocol_version, "runtime": runtime, "optimizations": optimizations, "options": options, + "executable_search_paths": executable_search_paths } }) @@ -159,6 +175,54 @@ def _get_container_dirs(source_dir, manifest_dir): return result + @staticmethod + def _convert_to_container_dirs(host_paths_to_convert, host_to_container_path_mapping): + """ + Use this method to convert a list of host paths to a list of equivalent paths within the container + where the given host path is mounted. This is necessary when SAM CLI needs to pass path information to + the Lambda Builder running within the container. + + If a host path is not mounted within the container, then this method simply passes the path to the result + without any changes. + + Ex: + [ "/home/foo", "/home/bar", "/home/not/mounted"] => ["/tmp/source", "/tmp/manifest", "/home/not/mounted"] + + Parameters + ---------- + host_paths_to_convert : list + List of paths in host that needs to be converted + + host_to_container_path_mapping : dict + Mapping of paths in host to the equivalent paths within the container + + Returns + ------- + list + Equivalent paths within the container + """ + + if not host_paths_to_convert: + # Nothing to do + return host_paths_to_convert + + # Make sure the key is absolute host path. Relative paths are tricky to work with because two different + # relative paths can point to the same directory ("../foo", "../../foo") + mapping = {str(pathlib.Path(p).resolve()): v for p, v in host_to_container_path_mapping.items()} + + result = [] + for original_path in host_paths_to_convert: + abspath = str(pathlib.Path(original_path).resolve()) + + if abspath in mapping: + result.append(mapping[abspath]) + else: + result.append(original_path) + LOG.debug("Cannot convert host path '%s' to its equivalent path within the container. " + "Host path is not mounted within the container", abspath) + + return result + @staticmethod def _get_image(runtime): return "{}:build-{}".format(LambdaBuildContainer._IMAGE_REPO_NAME, runtime) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/template.yaml b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/template.yaml index 29b766d1a4..f6367f0594 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/template.yaml +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/template.yaml @@ -1,46 +1,44 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > - Sample SAM Template for {{ cookiecutter.project_name }} + Sample SAM Template for {{ cookiecutter.project_name }} # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: - Function: - Timeout: 10 - + Function: + Timeout: 10 Resources: - - HelloWorldFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction - Properties: - CodeUri: ./artifacts/HelloWorld.zip - Handler: HelloWorld::HelloWorld.Function::FunctionHandler - {%- if cookiecutter.runtime == 'dotnetcore2.0' %} - Runtime: {{ cookiecutter.runtime }} - {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} - Runtime: dotnetcore2.1 - {%- endif %} - Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object - Variables: - PARAM1: VALUE - 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 - Properties: - Path: /hello - Method: get + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: ./artifacts/HelloWorld.zip + Handler: HelloWorld::HelloWorld.Function::FunctionHandler + {%- if cookiecutter.runtime == 'dotnetcore2.0' %} + Runtime: {{ cookiecutter.runtime }} + {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} + Runtime: dotnetcore2.1 + {%- endif %} + Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object + Variables: + PARAM1: VALUE + 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 + Properties: + Path: /hello + Method: get Outputs: - - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-golang/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-golang/{{cookiecutter.project_name}}/README.md index 36ec102297..2d783abbc2 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-golang/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-golang/{{cookiecutter.project_name}}/README.md @@ -32,7 +32,7 @@ go get -u github.com/aws/aws-lambda-go/... ### Building -Golang is a staticly compiled language, meaning that in order to run it you have to build the executeable target. +Golang is a statically compiled language, meaning that in order to run it you have to build the executable target. You can issue the following command in a shell to build it: @@ -40,7 +40,7 @@ You can issue the following command in a shell to build it: GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world ``` -**NOTE**: If you're not building the function on a Linux machine, you will need to specify the `GOOS` and `GOARCH` environment variables, this allows Golang to build your function for another system architecture and ensure compatability. +**NOTE**: If you're not building the function on a Linux machine, you will need to specify the `GOOS` and `GOARCH` environment variables, this allows Golang to build your function for another system architecture and ensure compatibility. ### Local development diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-golang/{{cookiecutter.project_name}}/template.yaml b/samcli/local/init/templates/cookiecutter-aws-sam-hello-golang/{{cookiecutter.project_name}}/template.yaml index 96d7aa160b..ff3edc4574 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-golang/{{cookiecutter.project_name}}/template.yaml +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-golang/{{cookiecutter.project_name}}/template.yaml @@ -29,14 +29,15 @@ Resources: PARAM1: VALUE Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api HelloWorldAPI: Description: "API Gateway endpoint URL for Prod environment for First Function" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - HelloWorldFunction: Description: "First Lambda Function ARN" Value: !GetAtt HelloWorldFunction.Arn - HelloWorldFunctionIamRole: Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn \ No newline at end of file + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-java/{{cookiecutter.project_name}}/template.yaml b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java/{{cookiecutter.project_name}}/template.yaml index bd687f3e32..080a1e6e84 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-java/{{cookiecutter.project_name}}/template.yaml +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java/{{cookiecutter.project_name}}/template.yaml @@ -1,44 +1,42 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > - {{ cookiecutter.project_name }} + {{ cookiecutter.project_name }} - Sample SAM Template for {{ cookiecutter.project_name }} + Sample SAM Template for {{ cookiecutter.project_name }} # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: - Function: - Timeout: 20 - + Function: + Timeout: 20 Resources: - - HelloWorldFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction - Properties: - CodeUri: target/HelloWorld-1.0.jar - Handler: helloworld.App::handleRequest - Runtime: java8 - Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object - Variables: - PARAM1: VALUE - 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 - Properties: - Path: /hello - Method: get + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: target/HelloWorld-1.0.jar + Handler: helloworld.App::handleRequest + Runtime: java8 + Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object + Variables: + PARAM1: VALUE + 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 + Properties: + Path: /hello + Method: get Outputs: - - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn 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 a6a15cecdd..a39cebc63c 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 @@ -1,44 +1,39 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > - {{ cookiecutter.project_name }} + {{ cookiecutter.project_name }} - Sample SAM Template for {{ cookiecutter.project_name }} - + Sample SAM Template for {{ cookiecutter.project_name }} + # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: - Function: - Timeout: 3 - + Function: + Timeout: 3 Resources: - - HelloWorldFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction - Properties: - CodeUri: hello-world/ - Handler: app.lambdaHandler - Runtime: nodejs8.10 - 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 - Properties: - Path: /hello - Method: get + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello-world/ + Handler: app.lambdaHandler + Runtime: nodejs8.10 + 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 + Properties: + Path: /hello + Method: get Outputs: - - # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function - # Find out more about other implicit resources you can reference within SAM - # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs6/{{cookiecutter.project_name}}/template.yaml b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs6/{{cookiecutter.project_name}}/template.yaml index a3e1ecd758..861221a315 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs6/{{cookiecutter.project_name}}/template.yaml +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs6/{{cookiecutter.project_name}}/template.yaml @@ -1,44 +1,39 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > - {{ cookiecutter.project_name }} + {{ cookiecutter.project_name }} - Sample SAM Template for {{ cookiecutter.project_name }} + Sample SAM Template for {{ cookiecutter.project_name }} # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: - Function: - Timeout: 3 - + Function: + Timeout: 3 Resources: - - HelloWorldFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction - Properties: - CodeUri: hello-world/ - Handler: app.lambdaHandler - Runtime: {{ cookiecutter.runtime }} - 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 - Properties: - Path: /hello - Method: get + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello-world/ + Handler: app.lambdaHandler + Runtime: {{ cookiecutter.runtime }} + 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 + Properties: + Path: /hello + Method: get Outputs: - - # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function - # Find out more about other implicit resources you can reference within SAM - # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/README.md index 5170effb56..d85255a94c 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/README.md @@ -1,6 +1,6 @@ # Cookiecutter Python Hello-world for SAM based Serverless App -A cookiecutter template to create a NodeJS Hello world boilerplate using [Serverless Application Model (SAM)](https://github.com/awslabs/serverless-application-model). +A cookiecutter template to create a Python Hello world boilerplate using [Serverless Application Model (SAM)](https://github.com/awslabs/serverless-application-model). ## Requirements diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/template.yaml b/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/template.yaml index d8cd417ea1..48c274e192 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/template.yaml +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/template.yaml @@ -1,50 +1,45 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > - {{ cookiecutter.project_name }} + {{ cookiecutter.project_name }} - Sample SAM Template for {{ cookiecutter.project_name }} + Sample SAM Template for {{ cookiecutter.project_name }} # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: - Function: - Timeout: 3 - + Function: + Timeout: 3 Resources: - - HelloWorldFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction - Properties: - CodeUri: hello_world/ - Handler: app.lambda_handler - {%- if cookiecutter.runtime == 'python2.7' %} - Runtime: python2.7 - {%- elif cookiecutter.runtime == 'python3.6' %} - Runtime: python3.6 - {%- else %} - Runtime: python3.7 - {%- 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 - Properties: - Path: /hello - Method: get + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler + {%- if cookiecutter.runtime == 'python2.7' %} + Runtime: python2.7 + {%- elif cookiecutter.runtime == 'python3.6' %} + Runtime: python3.6 + {%- else %} + Runtime: python3.7 + {%- 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 + Properties: + Path: /hello + Method: get Outputs: - - # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function - # Find out more about other implicit resources you can reference within SAM - # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/template.yaml b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/template.yaml index 6f798b1b97..1c2f0c8c6f 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/template.yaml +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/template.yaml @@ -1,44 +1,39 @@ AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > - {{ cookiecutter.project_name }} + {{ cookiecutter.project_name }} - Sample SAM Template for {{ cookiecutter.project_name }} + Sample SAM Template for {{ cookiecutter.project_name }} # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: - Function: - Timeout: 3 - + Function: + Timeout: 3 Resources: - - HelloWorldFunction: - Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction - Properties: - CodeUri: hello_world/ - Handler: app.lambda_handler - Runtime: ruby2.5 - 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 - Properties: - Path: /hello - Method: get + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler + Runtime: ruby2.5 + 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 + Properties: + Path: /hello + Method: get Outputs: - - # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function - # Find out more about other implicit resources you can reference within SAM - # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/setup.py b/setup.py index 67f52049d3..30cb2e7b6d 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def read_version(): author_email='aws-sam-developers@amazon.com', url='https://github.com/awslabs/aws-sam-cli', license='Apache License 2.0', - packages=find_packages(exclude=('tests', 'docs')), + packages=find_packages(exclude=['tests.*', 'tests']), keywords="AWS SAM CLI", # Support Python 2.7 and 3.6 or greater python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*', diff --git a/tests/integration/buildcmd/build_integ_base.py b/tests/integration/buildcmd/build_integ_base.py index d9031d3389..a40569455a 100644 --- a/tests/integration/buildcmd/build_integ_base.py +++ b/tests/integration/buildcmd/build_integ_base.py @@ -1,6 +1,10 @@ import os import shutil import tempfile +import logging +import subprocess +import json +from unittest import TestCase import docker @@ -9,7 +13,10 @@ except ImportError: from pathlib2 import Path -from unittest import TestCase +from samcli.yamlhelper import yaml_parse + + +LOG = logging.getLogger(__name__) class BuildIntegBase(TestCase): @@ -85,3 +92,21 @@ def _make_parameter_override_arg(self, overrides): return " ".join([ "ParameterKey={},ParameterValue={}".format(key, value) for key, value in overrides.items() ]) + + def _verify_resource_property(self, template_path, logical_id, property, expected_value): + + with open(template_path, 'r') as fp: + template_dict = yaml_parse(fp.read()) + self.assertEquals(expected_value, template_dict["Resources"][logical_id]["Properties"][property]) + + def _verify_invoke_built_function(self, template_path, function_logical_id, overrides, expected_result): + LOG.info("Invoking built function '{}'", function_logical_id) + + cmdlist = [self.cmd, "local", "invoke", function_logical_id, "-t", str(template_path), "--no-event", + "--parameter-overrides", overrides] + + process = subprocess.Popen(cmdlist, stdout=subprocess.PIPE) + process.wait() + + process_stdout = b"".join(process.stdout.readlines()).strip().decode('utf-8') + self.assertEquals(json.loads(process_stdout), expected_result) diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 2397e63dfe..b8b5183968 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -1,7 +1,6 @@ import sys import os import subprocess -import json import logging try: @@ -10,7 +9,6 @@ from pathlib2 import Path from parameterized import parameterized -from samcli.yamlhelper import yaml_parse from .build_integ_base import BuildIntegBase @@ -41,7 +39,7 @@ def test_with_default_requirements(self, runtime, use_container): self.skipTest("Current Python version '{}' does not match Lambda runtime version '{}'".format(py_version, runtime)) - overrides = {"Runtime": runtime, "CodeUri": "Python"} + overrides = {"Runtime": runtime, "CodeUri": "Python", "Handler": "main.handler"} cmdlist = self.get_command_list(use_container=use_container, parameter_overrides=overrides) @@ -70,19 +68,6 @@ def test_with_default_requirements(self, runtime, use_container): expected) self.verify_docker_container_cleanedup(runtime) - def _verify_invoke_built_function(self, template_path, function_logical_id, overrides, expected_result): - LOG.info("Invoking built function '{}'", function_logical_id) - - cmdlist = [self.cmd, "local", "invoke", function_logical_id, "-t", str(template_path), "--no-event", - "--parameter-overrides", overrides] - - process = subprocess.Popen(cmdlist, stdout=subprocess.PIPE) - process.wait() - - process_stdout = b"".join(process.stdout.readlines()).strip().decode('utf-8') - print(process_stdout) - self.assertEquals(json.loads(process_stdout), expected_result) - def _verify_built_artifact(self, build_dir, function_logical_id, expected_files): self.assertTrue(build_dir.exists(), "Build directory should be created") @@ -105,12 +90,6 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files) actual_files = all_artifacts.intersection(expected_files) self.assertEquals(actual_files, expected_files) - def _verify_resource_property(self, template_path, logical_id, property, expected_value): - - with open(template_path, 'r') as fp: - template_dict = yaml_parse(fp.read()) - self.assertEquals(expected_value, template_dict["Resources"][logical_id]["Properties"][property]) - def _get_python_version(self): return "python{}.{}".format(sys.version_info.major, sys.version_info.minor) @@ -148,7 +127,7 @@ class TestBuildCommand_NodeFunctions(BuildIntegBase): ("nodejs8.10", "use_container") ]) def test_with_default_package_json(self, runtime, use_container): - overrides = {"Runtime": runtime, "CodeUri": "Node"} + overrides = {"Runtime": runtime, "CodeUri": "Node", "Handler": "ignored"} cmdlist = self.get_command_list(use_container=use_container, parameter_overrides=overrides) @@ -193,12 +172,6 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files, actual_files = all_modules.intersection(expected_modules) self.assertEquals(actual_files, expected_modules) - def _verify_resource_property(self, template_path, logical_id, property, expected_value): - - with open(template_path, 'r') as fp: - template_dict = yaml_parse(fp.read()) - self.assertEquals(expected_value, template_dict["Resources"][logical_id]["Properties"][property]) - class TestBuildCommand_RubyFunctions(BuildIntegBase): @@ -213,7 +186,7 @@ class TestBuildCommand_RubyFunctions(BuildIntegBase): ("ruby2.5", "use_container") ]) def test_with_default_gemfile(self, runtime, use_container): - overrides = {"Runtime": runtime, "CodeUri": "Ruby"} + overrides = {"Runtime": runtime, "CodeUri": "Ruby", "Handler": "ignored"} cmdlist = self.get_command_list(use_container=use_container, parameter_overrides=overrides) @@ -266,8 +239,70 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files, self.assertTrue(any([True if self.EXPECTED_RUBY_GEM in gem else False for gem in os.listdir(str(gem_path))])) - def _verify_resource_property(self, template_path, logical_id, property, expected_value): - with open(template_path, 'r') as fp: - template_dict = yaml_parse(fp.read()) - self.assertEquals(expected_value, template_dict["Resources"][logical_id]["Properties"][property]) +class TestBuildCommand_JavaGradle(BuildIntegBase): + + EXPECTED_FILES_PROJECT_MANIFEST = {'aws', 'lib', "META-INF"} + EXPECTED_DEPENDENCIES = {'annotations-2.1.0.jar', "aws-lambda-java-core-1.1.0.jar"} + + FUNCTION_LOGICAL_ID = "Function" + USING_GRADLE_PATH = os.path.join("Java", "gradle") + USING_GRADLEW_PATH = os.path.join("Java", "gradlew") + + @parameterized.expand([ + ("java8", USING_GRADLE_PATH, False), + ("java8", USING_GRADLEW_PATH, False), + ("java8", USING_GRADLE_PATH, "use_container"), + ("java8", USING_GRADLEW_PATH, "use_container"), + ]) + def test_with_gradle(self, runtime, code_path, use_container): + overrides = {"Runtime": runtime, "CodeUri": code_path, "Handler": "aws.example.Hello::myHandler"} + cmdlist = self.get_command_list(use_container=use_container, + parameter_overrides=overrides) + + LOG.info("Running Command: {}".format(cmdlist)) + process = subprocess.Popen(cmdlist, cwd=self.working_dir) + process.wait() + + self._verify_built_artifact(self.default_build_dir, self.FUNCTION_LOGICAL_ID, + self.EXPECTED_FILES_PROJECT_MANIFEST, self.EXPECTED_DEPENDENCIES) + + self._verify_resource_property(str(self.built_template), + "OtherRelativePathResource", + "BodyS3Location", + os.path.relpath( + os.path.normpath(os.path.join(str(self.test_data_path), "SomeRelativePath")), + str(self.default_build_dir)) + ) + + expected = "Hello World" + 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, expected_modules): + + 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) + + lib_dir_contents = set(os.listdir(str(resource_artifact_dir.joinpath("lib")))) + self.assertEquals(lib_dir_contents, expected_modules) diff --git a/tests/integration/local/invoke/test_integrations_cli.py b/tests/integration/local/invoke/test_integrations_cli.py index d39e400a8f..fbbb03441f 100644 --- a/tests/integration/local/invoke/test_integrations_cli.py +++ b/tests/integration/local/invoke/test_integrations_cli.py @@ -3,6 +3,7 @@ import os import copy from unittest import skipIf +import tempfile from nose_parameterized import parameterized from subprocess import Popen, PIPE @@ -232,6 +233,208 @@ def test_invoke_with_docker_network_of_host(self): self.assertEquals(return_code, 0) + def test_invoke_with_docker_network_of_host_in_env_var(self): + command_list = self.get_command_list("HelloWorldServerlessFunction", + template_path=self.template_path, + event_path=self.event_path) + + env = os.environ.copy() + env["SAM_DOCKER_NETWORK"] = 'non-existing-network' + + process = Popen(command_list, stderr=PIPE, env=env) + process.wait() + process_stderr = b"".join(process.stderr.readlines()).strip() + + self.assertIn('Not Found ("network non-existing-network not found")', process_stderr.decode('utf-8')) + + def test_sam_template_file_env_var_set(self): + command_list = self.get_command_list("HelloWorldFunctionInNonDefaultTemplate", event_path=self.event_path) + + self.test_data_path.joinpath("invoke", "sam-template.yaml") + env = os.environ.copy() + env["SAM_TEMPLATE_FILE"] = str(self.test_data_path.joinpath("invoke", "sam-template.yaml")) + + process = Popen(command_list, stdout=PIPE, env=env) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + + self.assertEquals(process_stdout.decode('utf-8'), '"Hello world"') + + def test_skip_pull_image_in_env_var(self): + docker.from_env().api.pull('lambci/lambda:python3.6') + + command_list = self.get_command_list("HelloWorldLambdaFunction", + template_path=self.template_path, + event_path=self.event_path) + + env = os.environ.copy() + env["SAM_SKIP_PULL_IMAGE"] = "True" + + process = Popen(command_list, stderr=PIPE, env=env) + process.wait() + process_stderr = b"".join(process.stderr.readlines()).strip() + self.assertIn("Requested to skip pulling images", process_stderr.decode('utf-8')) + + +class TestUsingConfigFiles(InvokeIntegBase): + template = Path("template.yml") + + def setUp(self): + self.config_dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.config_dir, ignore_errors=True) + + def test_existing_env_variables_precedence_over_profiles(self): + profile = "default" + custom_config = self._create_config_file(profile) + custom_cred = self._create_cred_file(profile) + + command_list = self.get_command_list("EchoEnvWithParameters", + template_path=self.template_path, + event_path=self.event_path) + + env = os.environ.copy() + + # Explicitly set environment variables beforehand + env['AWS_DEFAULT_REGION'] = 'sa-east-1' + env['AWS_REGION'] = 'sa-east-1' + env['AWS_ACCESS_KEY_ID'] = 'priority_access_key_id' + env['AWS_SECRET_ACCESS_KEY'] = 'priority_secret_key_id' + env['AWS_SESSION_TOKEN'] = 'priority_secret_token' + + # Setup a custom profile + env['AWS_CONFIG_FILE'] = custom_config + env['AWS_SHARED_CREDENTIALS_FILE'] = custom_cred + + process = Popen(command_list, stdout=PIPE, env=env) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + environ = json.loads(process_stdout.decode('utf-8')) + + # Environment variables we explicitly set take priority over profiles. + self.assertEquals(environ["AWS_DEFAULT_REGION"], 'sa-east-1') + self.assertEquals(environ["AWS_REGION"], 'sa-east-1') + self.assertEquals(environ["AWS_ACCESS_KEY_ID"], 'priority_access_key_id') + self.assertEquals(environ["AWS_SECRET_ACCESS_KEY"], 'priority_secret_key_id') + self.assertEquals(environ["AWS_SESSION_TOKEN"], 'priority_secret_token') + + def test_default_profile_with_custom_configs(self): + profile = "default" + custom_config = self._create_config_file(profile) + custom_cred = self._create_cred_file(profile) + + command_list = self.get_command_list("EchoEnvWithParameters", + template_path=self.template_path, + event_path=self.event_path) + + env = os.environ.copy() + + # Explicitly clean environment variables beforehand + env.pop('AWS_DEFAULT_REGION', None) + env.pop('AWS_REGION', None) + env.pop('AWS_ACCESS_KEY_ID', None) + env.pop('AWS_SECRET_ACCESS_KEY', None) + env.pop('AWS_SESSION_TOKEN', None) + env['AWS_CONFIG_FILE'] = custom_config + env['AWS_SHARED_CREDENTIALS_FILE'] = custom_cred + + process = Popen(command_list, stdout=PIPE, env=env) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + environ = json.loads(process_stdout.decode('utf-8')) + + self.assertEquals(environ["AWS_DEFAULT_REGION"], 'us-west-1') + self.assertEquals(environ["AWS_REGION"], 'us-west-1') + self.assertEquals(environ["AWS_ACCESS_KEY_ID"], 'someaccesskeyid') + self.assertEquals(environ["AWS_SECRET_ACCESS_KEY"], 'shhhhhthisisasecret') + self.assertEquals(environ["AWS_SESSION_TOKEN"], 'sessiontoken') + + def test_custom_profile_with_custom_configs(self): + custom_config = self._create_config_file("custom") + custom_cred = self._create_cred_file("custom") + + command_list = self.get_command_list("EchoEnvWithParameters", + template_path=self.template_path, + event_path=self.event_path, + profile='custom') + + env = os.environ.copy() + + # Explicitly clean environment variables beforehand + env.pop('AWS_DEFAULT_REGION', None) + env.pop('AWS_REGION', None) + env.pop('AWS_ACCESS_KEY_ID', None) + env.pop('AWS_SECRET_ACCESS_KEY', None) + env.pop('AWS_SESSION_TOKEN', None) + env['AWS_CONFIG_FILE'] = custom_config + env['AWS_SHARED_CREDENTIALS_FILE'] = custom_cred + + process = Popen(command_list, stdout=PIPE, env=env) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + environ = json.loads(process_stdout.decode('utf-8')) + + self.assertEquals(environ["AWS_DEFAULT_REGION"], 'us-west-1') + self.assertEquals(environ["AWS_REGION"], 'us-west-1') + self.assertEquals(environ["AWS_ACCESS_KEY_ID"], 'someaccesskeyid') + self.assertEquals(environ["AWS_SECRET_ACCESS_KEY"], 'shhhhhthisisasecret') + self.assertEquals(environ["AWS_SESSION_TOKEN"], 'sessiontoken') + + def test_custom_profile_through_envrionment_variables(self): + # When using a custom profile in a custom location, you need both the config + # and credential file otherwise we fail to find a region or the profile (depending + # on which one is provided + custom_config = self._create_config_file("custom") + + custom_cred = self._create_cred_file("custom") + + command_list = self.get_command_list("EchoEnvWithParameters", + template_path=self.template_path, + event_path=self.event_path) + + env = os.environ.copy() + + # Explicitly clean environment variables beforehand + env.pop('AWS_DEFAULT_REGION', None) + env.pop('AWS_REGION', None) + env.pop('AWS_ACCESS_KEY_ID', None) + env.pop('AWS_SECRET_ACCESS_KEY', None) + env.pop('AWS_SESSION_TOKEN', None) + env['AWS_CONFIG_FILE'] = custom_config + env['AWS_SHARED_CREDENTIALS_FILE'] = custom_cred + env['AWS_PROFILE'] = "custom" + + process = Popen(command_list, stdout=PIPE, env=env) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + environ = json.loads(process_stdout.decode('utf-8')) + + self.assertEquals(environ["AWS_DEFAULT_REGION"], 'us-west-1') + self.assertEquals(environ["AWS_REGION"], 'us-west-1') + self.assertEquals(environ["AWS_ACCESS_KEY_ID"], 'someaccesskeyid') + self.assertEquals(environ["AWS_SECRET_ACCESS_KEY"], 'shhhhhthisisasecret') + self.assertEquals(environ["AWS_SESSION_TOKEN"], 'sessiontoken') + + def _create_config_file(self, profile): + if profile == "default": + config_file_content = "[{}]\noutput = json\nregion = us-west-1".format(profile) + else: + config_file_content = "[profile {}]\noutput = json\nregion = us-west-1".format(profile) + + custom_config = os.path.join(self.config_dir, "customconfig") + with open(custom_config, "w") as file: + file.write(config_file_content) + return custom_config + + def _create_cred_file(self, profile): + cred_file_content = "[{}]\naws_access_key_id = someaccesskeyid\naws_secret_access_key = shhhhhthisisasecret \ + \naws_session_token = sessiontoken".format(profile) + custom_cred = os.path.join(self.config_dir, "customcred") + with open(custom_cred, "w") as file: + file.write(cred_file_content) + return custom_cred + @skipIf(SKIP_LAYERS_TESTS, "Skip layers tests in Travis only") @@ -393,6 +596,23 @@ def test_caching_two_layers(self): self.assertEquals(2, len(os.listdir(str(self.layer_cache)))) + def test_caching_two_layers_with_layer_cache_env_set(self): + + command_list = self.get_command_list("TwoLayerVersionServerlessFunction", + template_path=self.template_path, + no_event=True, + region=self.region, + parameter_overrides=self.layer_utils.parameters_overrides + ) + + env = os.environ.copy() + env["SAM_LAYER_CACHE_BASEDIR"] = str(self.layer_cache) + + process = Popen(command_list, stdout=PIPE, env=env) + process.wait() + + self.assertEquals(2, len(os.listdir(str(self.layer_cache)))) + @skipIf(SKIP_LAYERS_TESTS, "Skip layers tests in Travis only") diff --git a/tests/integration/local/start_api/test_start_api.py b/tests/integration/local/start_api/test_start_api.py index c32c2429f8..577c6acbcb 100644 --- a/tests/integration/local/start_api/test_start_api.py +++ b/tests/integration/local/start_api/test_start_api.py @@ -324,6 +324,14 @@ def test_default_status_code(self): self.assertEquals(response.status_code, 200) self.assertEquals(response.json(), {'hello': 'world'}) + def test_string_status_code(self): + """ + Test that an integer-string can be returned as the status code + """ + response = requests.get(self.url + "/stringstatuscode") + + self.assertEquals(response.status_code, 200) + def test_default_body(self): """ Test that if no body is given, the response is 'no data' diff --git a/tests/integration/testdata/buildcmd/Java/gradle/build.gradle b/tests/integration/testdata/buildcmd/Java/gradle/build.gradle new file mode 100644 index 0000000000..ee63ee0c43 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradle/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'software.amazon.awssdk:annotations:2.1.0' + compile ( + 'com.amazonaws:aws-lambda-java-core:1.1.0' + ) +} diff --git a/tests/integration/testdata/buildcmd/Java/gradle/src/main/java/aws/example/Hello.java b/tests/integration/testdata/buildcmd/Java/gradle/src/main/java/aws/example/Hello.java new file mode 100644 index 0000000000..db02d37583 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradle/src/main/java/aws/example/Hello.java @@ -0,0 +1,13 @@ +package aws.example; + + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; + +public class Hello { + public String myHandler(Context context) { + LambdaLogger logger = context.getLogger(); + logger.log("Function Invoked\n"); + return "Hello World"; + } +} diff --git a/tests/integration/testdata/buildcmd/Java/gradlew/build.gradle b/tests/integration/testdata/buildcmd/Java/gradlew/build.gradle new file mode 100644 index 0000000000..ee63ee0c43 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradlew/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'software.amazon.awssdk:annotations:2.1.0' + compile ( + 'com.amazonaws:aws-lambda-java-core:1.1.0' + ) +} diff --git a/tests/integration/testdata/buildcmd/Java/gradlew/gradle/wrapper/gradle-wrapper.jar b/tests/integration/testdata/buildcmd/Java/gradlew/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..87b738cbd0 Binary files /dev/null and b/tests/integration/testdata/buildcmd/Java/gradlew/gradle/wrapper/gradle-wrapper.jar differ diff --git a/tests/integration/testdata/buildcmd/Java/gradlew/gradle/wrapper/gradle-wrapper.properties b/tests/integration/testdata/buildcmd/Java/gradlew/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..558870dad5 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradlew/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/tests/integration/testdata/buildcmd/Java/gradlew/gradlew b/tests/integration/testdata/buildcmd/Java/gradlew/gradlew new file mode 100755 index 0000000000..af6708ff22 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradlew/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/tests/integration/testdata/buildcmd/Java/gradlew/gradlew.bat b/tests/integration/testdata/buildcmd/Java/gradlew/gradlew.bat new file mode 100644 index 0000000000..0f8d5937c4 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradlew/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/tests/integration/testdata/buildcmd/Java/gradlew/src/main/java/aws/example/Hello.java b/tests/integration/testdata/buildcmd/Java/gradlew/src/main/java/aws/example/Hello.java new file mode 100644 index 0000000000..db02d37583 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradlew/src/main/java/aws/example/Hello.java @@ -0,0 +1,13 @@ +package aws.example; + + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; + +public class Hello { + public String myHandler(Context context) { + LambdaLogger logger = context.getLogger(); + logger.log("Function Invoked\n"); + return "Hello World"; + } +} diff --git a/tests/integration/testdata/buildcmd/template.yaml b/tests/integration/testdata/buildcmd/template.yaml index 9682db822b..fb85168ff4 100644 --- a/tests/integration/testdata/buildcmd/template.yaml +++ b/tests/integration/testdata/buildcmd/template.yaml @@ -6,13 +6,15 @@ Parameteres: Type: String CodeUri: Type: String + Handler: + Type: String Resources: Function: Type: AWS::Serverless::Function Properties: - Handler: main.handler + Handler: !Ref Handler Runtime: !Ref Runtime CodeUri: !Ref CodeUri Timeout: 600 diff --git a/tests/integration/testdata/invoke/sam-template.yaml b/tests/integration/testdata/invoke/sam-template.yaml new file mode 100644 index 0000000000..24b07ff369 --- /dev/null +++ b/tests/integration/testdata/invoke/sam-template.yaml @@ -0,0 +1,12 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A hello world application. + +Resources: + HelloWorldFunctionInNonDefaultTemplate: + Type: AWS::Serverless::Function + Properties: + Handler: main.handler + Runtime: python3.6 + CodeUri: . + Timeout: 600 diff --git a/tests/integration/testdata/start_api/main.py b/tests/integration/testdata/start_api/main.py index 98e1cae3f6..9304ec8495 100644 --- a/tests/integration/testdata/start_api/main.py +++ b/tests/integration/testdata/start_api/main.py @@ -35,6 +35,11 @@ def only_set_body_handler(event, context): return {"body": json.dumps({"hello": "world"})} +def string_status_code_handler(event, context): + + return {"statusCode": "200", "body": json.dumps({"hello": "world"})} + + def sleep_10_sec_handler(event, context): # sleep thread for 10s. This is useful for testing multiple requests time.sleep(10) diff --git a/tests/integration/testdata/start_api/template.yaml b/tests/integration/testdata/start_api/template.yaml index 8ece84b2cf..d3ec04ec57 100644 --- a/tests/integration/testdata/start_api/template.yaml +++ b/tests/integration/testdata/start_api/template.yaml @@ -111,6 +111,19 @@ Resources: Method: Get Path: /onlysetbody + StringStatusCodeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.string_status_code_handler + Runtime: python3.6 + CodeUri: . + Events: + StringStatusCodePath: + Type: Api + Properties: + Method: Get + Path: /stringstatuscode + SleepFunction0: Type: AWS::Serverless::Function Properties: diff --git a/tests/unit/commands/buildcmd/test_command.py b/tests/unit/commands/buildcmd/test_command.py index 3c6700fb6d..00e1b2c112 100644 --- a/tests/unit/commands/buildcmd/test_command.py +++ b/tests/unit/commands/buildcmd/test_command.py @@ -5,7 +5,8 @@ from samcli.commands.build.command import do_cli from samcli.commands.exceptions import UserException -from samcli.lib.build.app_builder import UnsupportedRuntimeException, BuildError, UnsupportedBuilderLibraryVersionError +from samcli.lib.build.app_builder import BuildError, UnsupportedBuilderLibraryVersionError +from samcli.lib.build.workflow_config import UnsupportedRuntimeException class TestDoCli(TestCase): diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index 0229f4787d..28bdb8298f 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -5,52 +5,12 @@ from unittest import TestCase from mock import Mock, call, patch -from parameterized import parameterized -from samcli.lib.build.app_builder import ApplicationBuilder, _get_workflow_config,\ - UnsupportedBuilderLibraryVersionError, UnsupportedRuntimeException, BuildError, \ +from samcli.lib.build.app_builder import ApplicationBuilder,\ + UnsupportedBuilderLibraryVersionError, BuildError, \ LambdaBuilderError -class Test_get_workflow_config(TestCase): - - @parameterized.expand([ - ("python2.7", ), - ("python3.6", ) - ]) - def test_must_work_for_python(self, runtime): - - result = _get_workflow_config(runtime) - self.assertEquals(result.language, "python") - self.assertEquals(result.dependency_manager, "pip") - self.assertEquals(result.application_framework, None) - self.assertEquals(result.manifest_name, "requirements.txt") - - @parameterized.expand([ - ("nodejs6.10", ), - ("nodejs8.10", ), - ("nodejsX.Y", ), - ("nodejs", ) - ]) - def test_must_work_for_nodejs(self, runtime): - - result = _get_workflow_config(runtime) - self.assertEquals(result.language, "nodejs") - self.assertEquals(result.dependency_manager, "npm") - self.assertEquals(result.application_framework, None) - self.assertEquals(result.manifest_name, "package.json") - - def test_must_raise_for_unsupported_runtimes(self): - - runtime = "foobar" - - with self.assertRaises(UnsupportedRuntimeException) as ctx: - _get_workflow_config(runtime) - - self.assertEquals(str(ctx.exception), - "'foobar' runtime is not supported") - - class TestApplicationBuilder_build(TestCase): def setUp(self): @@ -158,7 +118,7 @@ def setUp(self): "/build/dir", "/base/dir") - @patch("samcli.lib.build.app_builder._get_workflow_config") + @patch("samcli.lib.build.app_builder.get_workflow_config") @patch("samcli.lib.build.app_builder.osutils") def test_must_build_in_process(self, osutils_mock, get_workflow_config_mock): function_name = "function_name" @@ -186,7 +146,7 @@ def test_must_build_in_process(self, osutils_mock, get_workflow_config_mock): manifest_path, runtime) - @patch("samcli.lib.build.app_builder._get_workflow_config") + @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): function_name = "function_name" @@ -245,13 +205,15 @@ def test_must_use_lambda_builder(self, lambda_builder_mock): "artifacts_dir", "scratch_dir", "manifest_path", - runtime="runtime") + runtime="runtime", + executable_search_paths=config_mock.executable_search_paths) @patch("samcli.lib.build.app_builder.LambdaBuilder") def test_must_raise_on_error(self, lambda_builder_mock): config_mock = Mock() builder_instance_mock = lambda_builder_mock.return_value = Mock() builder_instance_mock.build.side_effect = LambdaBuilderError() + self.builder._get_build_options = Mock(return_value=None) with self.assertRaises(BuildError): self.builder._build_function_in_process(config_mock, @@ -312,7 +274,8 @@ def mock_wait_for_logs(stdout, stderr): "runtime", log_level=log_level, optimizations=None, - options=None) + options=None, + executable_search_paths=config.executable_search_paths) self.container_manager.run.assert_called_with(container_mock) self.builder._parse_builder_response.assert_called_once_with(stdout_data, container_mock.image) diff --git a/tests/unit/lib/build_module/test_workflow_config.py b/tests/unit/lib/build_module/test_workflow_config.py new file mode 100644 index 0000000000..0722ea8a45 --- /dev/null +++ b/tests/unit/lib/build_module/test_workflow_config.py @@ -0,0 +1,91 @@ +from unittest import TestCase +from parameterized import parameterized +from mock import patch + +from samcli.lib.build.workflow_config import get_workflow_config, UnsupportedRuntimeException + + +class Test_get_workflow_config(TestCase): + + def setUp(self): + self.code_dir = '' + self.project_dir = '' + + @parameterized.expand([ + ("python2.7", ), + ("python3.6", ) + ]) + def test_must_work_for_python(self, runtime): + + result = get_workflow_config(runtime, self.code_dir, self.project_dir) + self.assertEquals(result.language, "python") + self.assertEquals(result.dependency_manager, "pip") + self.assertEquals(result.application_framework, None) + self.assertEquals(result.manifest_name, "requirements.txt") + self.assertIsNone(result.executable_search_paths) + + @parameterized.expand([ + ("nodejs4.3", ), + ("nodejs6.10", ), + ("nodejs8.10", ), + ]) + def test_must_work_for_nodejs(self, runtime): + + result = get_workflow_config(runtime, self.code_dir, self.project_dir) + self.assertEquals(result.language, "nodejs") + self.assertEquals(result.dependency_manager, "npm") + self.assertEquals(result.application_framework, None) + self.assertEquals(result.manifest_name, "package.json") + self.assertIsNone(result.executable_search_paths) + + @parameterized.expand([ + ("ruby2.5", ) + ]) + def test_must_work_for_ruby(self, runtime): + result = get_workflow_config(runtime, self.code_dir, self.project_dir) + self.assertEquals(result.language, "ruby") + self.assertEquals(result.dependency_manager, "bundler") + self.assertEquals(result.application_framework, None) + self.assertEquals(result.manifest_name, "Gemfile") + self.assertIsNone(result.executable_search_paths) + + @parameterized.expand([ + ("java8", "build.gradle") + ]) + @patch("samcli.lib.build.workflow_config.os") + def test_must_work_for_java(self, runtime, build_file, os_mock): + + os_mock.path.join.side_effect = lambda dirname, v: v + os_mock.path.exists.side_effect = lambda v: v == build_file + + result = get_workflow_config(runtime, self.code_dir, self.project_dir) + self.assertEquals(result.language, "java") + self.assertEquals(result.dependency_manager, "gradle") + self.assertEquals(result.application_framework, None) + self.assertEquals(result.manifest_name, "build.gradle") + self.assertEquals(result.executable_search_paths, [self.code_dir, self.project_dir]) + + @parameterized.expand([ + ("java8", "unknown.manifest") + ]) + @patch("samcli.lib.build.workflow_config.os") + def test_must_fail_when_manifest_not_found(self, runtime, build_file, os_mock): + + os_mock.path.join.side_effect = lambda dirname, v: v + os_mock.path.exists.side_effect = lambda v: v == build_file + + with self.assertRaises(UnsupportedRuntimeException) as ctx: + get_workflow_config(runtime, self.code_dir, self.project_dir) + + self.assertIn("Unable to find a supported build workflow for runtime '{}'.".format(runtime), + str(ctx.exception)) + + def test_must_raise_for_unsupported_runtimes(self): + + runtime = "foobar" + + with self.assertRaises(UnsupportedRuntimeException) as ctx: + get_workflow_config(runtime, self.code_dir, self.project_dir) + + self.assertEquals(str(ctx.exception), + "'foobar' runtime is not supported") diff --git a/tests/unit/local/apigw/test_local_apigw_service.py b/tests/unit/local/apigw/test_local_apigw_service.py index 234554deca..712674ff36 100644 --- a/tests/unit/local/apigw/test_local_apigw_service.py +++ b/tests/unit/local/apigw/test_local_apigw_service.py @@ -341,6 +341,15 @@ def test_status_code_not_int(self): binary_types=[], flask_request=Mock()) + def test_status_code_int_str(self): + lambda_output = '{"statusCode": "200", "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ + '"isBase64Encoded": false}' + + (status_code, _, _) = LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) + self.assertEquals(status_code, 200) + def test_status_code_negative_int(self): lambda_output = '{"statusCode": -1, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ '"isBase64Encoded": false}' @@ -350,6 +359,15 @@ def test_status_code_negative_int(self): binary_types=[], flask_request=Mock()) + def test_status_code_negative_int_str(self): + lambda_output = '{"statusCode": "-1", "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' \ + '"isBase64Encoded": false}' + + with self.assertRaises(TypeError): + LocalApigwService._parse_lambda_output(lambda_output, + binary_types=[], + flask_request=Mock()) + def test_lambda_output_list_not_dict(self): lambda_output = '[]' diff --git a/tests/unit/local/docker/test_lambda_build_container.py b/tests/unit/local/docker/test_lambda_build_container.py index 6c993ab086..8c5421b604 100644 --- a/tests/unit/local/docker/test_lambda_build_container.py +++ b/tests/unit/local/docker/test_lambda_build_container.py @@ -3,6 +3,11 @@ """ import json +try: + import pathlib +except ImportError: + import pathlib2 as pathlib + from unittest import TestCase from mock import patch @@ -87,7 +92,8 @@ def test_must_make_request_object_string(self): "manifest_file_name", "runtime", "optimizations", - "options") + "options", + "executable_search_paths") self.maxDiff = None # Print whole json diff self.assertEqual(json.loads(result), { @@ -107,7 +113,8 @@ def test_must_make_request_object_string(self): "manifest_path": "manifest_dir/manifest_file_name", "runtime": "runtime", "optimizations": "optimizations", - "options": "options" + "options": "options", + "executable_search_paths": "executable_search_paths" } }) @@ -155,3 +162,44 @@ class TestLambdaBuildContainer_get_entrypoint(TestCase): def test_must_get_entrypoint(self): self.assertEquals(["lambda-builders", "requestjson"], LambdaBuildContainer._get_entrypoint("requestjson")) + + +class TestLambdaBuildContainer_convert_to_container_dirs(TestCase): + + def test_must_work_on_abs_and_relative_paths(self): + + input = [".", "../foo", "/some/abs/path"] + mapping = { + str(pathlib.Path(".").resolve()): "/first", + "../foo": "/second", + "/some/abs/path": "/third" + } + + expected = ["/first", "/second", "/third"] + result = LambdaBuildContainer._convert_to_container_dirs(input, mapping) + + self.assertEquals(result, expected) + + def test_must_skip_unknown_paths(self): + + input = ["/known/path", "/unknown/path"] + mapping = { + "/known/path": "/first" + } + + expected = ["/first", "/unknown/path"] + result = LambdaBuildContainer._convert_to_container_dirs(input, mapping) + + self.assertEquals(result, expected) + + def test_must_skip_on_empty_input(self): + + input = None + mapping = { + "/known/path": "/first" + } + + expected = None + result = LambdaBuildContainer._convert_to_container_dirs(input, mapping) + + self.assertEquals(result, expected)