diff --git a/.gitignore b/.gitignore index 5389aa8947..93b3ed0745 100644 --- a/.gitignore +++ b/.gitignore @@ -380,5 +380,9 @@ $RECYCLE.BIN/ # Temporary scratch directory used by the tests tests/integration/buildcmd/scratch +tests/integration/testdata/buildcmd/Dotnetcore2.0/bin +tests/integration/testdata/buildcmd/Dotnetcore2.0/obj +tests/integration/testdata/buildcmd/Dotnetcore2.1/bin +tests/integration/testdata/buildcmd/Dotnetcore2.1/obj # End of https://www.gitignore.io/api/osx,node,macos,linux,python,windows,pycharm,intellij,sublimetext,visualstudiocode diff --git a/.travis.yml b/.travis.yml index 91fc368026..86e9e56e6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,15 @@ before_install: - nvm install 8.10 - npm --version - node --version + # Install .NET Core 2.1 + - export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 DOTNET_CLI_TELEMETRY_OPTOUT=1 + - if [ "$LINUX" ]; then sudo apt install libunwind8; fi + - wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh + - chmod +x /tmp/dotnet-install.sh + - /tmp/dotnet-install.sh -v 2.1.504 + - export DOTNET_ROOT=/home/travis/.dotnet + - export PATH=/home/travis/.dotnet:/home/travis/.dotnet/tools:$PATH + - dotnet --info install: # Install the code requirements diff --git a/README.md b/README.md index c2ad58f588..7dacf629fb 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,12 @@ SAM CLI (Beta) ![Build Status](https://travis-ci.org/awslabs/aws-sam-cli.svg?branch=develop) ![Apache-2.0](https://img.shields.io/npm/l/aws-sam-local.svg) -![Contributers](https://img.shields.io/github/contributors/awslabs/aws-sam-cli.svg) +![Contributors](https://img.shields.io/github/contributors/awslabs/aws-sam-cli.svg) ![GitHub-release](https://img.shields.io/github/release/awslabs/aws-sam-cli.svg) ![PyPI version](https://badge.fury.io/py/aws-sam-cli.svg) +[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/aws-sam-cli) + [Join the SAM developers channel (\#samdev) on Slack](https://join.slack.com/t/awsdevelopers/shared_invite/enQtMzg3NTc5OTM2MzcxLTdjYTdhYWE3OTQyYTU4Njk1ZWY4Y2ZjYjBhMTUxNGYzNDg5MWQ1ZTc5MTRlOGY0OTI4NTdlZTMwNmI5YTgwOGM/) to collaborate with fellow community members and the AWS SAM team. diff --git a/designs/build_debug_artifacts.md b/designs/build_debug_artifacts.md new file mode 100644 index 0000000000..01ba532f9e --- /dev/null +++ b/designs/build_debug_artifacts.md @@ -0,0 +1,135 @@ +SAM Build producing debuggable artifacts +======================================== + +What is the problem? +-------------------- + +`sam build` produces artifacts that are built for production use. In some langauges (usually interpreted), +these production artifacts are also debuggable locally. In the case of compiled languages, you usually need to compile +the binary or artifact in a specific manner for them to be debuggable. + +What will be changed? +--------------------- + +We will introduce a way in `sam build` to produce these debuggable artifacts for those compiled languages. + +Success criteria for the change +------------------------------- + +1. Artifacts generated will be debuggable for runtimes DotNetCore 2.0 and above. + +Out-of-Scope +------------ + +1. Other languages `sam build` supports will not be changed + +User Experience Walkthrough +--------------------------- + +Implementation +============== + +CLI Changes +----------- + +*Explain the changes to command line interface, including adding new +commands, modifying arguments etc* + +### Breaking Change + +Changes are additive and will not break any existing functionality. + +Design +------ + +Options considered +------------------ +We have a couple options to consider: + +1. A command line option (`--mode`) + * Pros + * Customer can control what kind of artifacts are produced. + * There is no guessing needed by the CLI + * Cons + * Yet another option for customers to learn or know about + * Makes the customer need to think about the artifacts they need to produce (though this can be hidden + behind the AWS Toolkit in IDEs) + * Customers running in the command line need to remember what artifacts to produce or what they previously produced +2. An Environment Variable we read (`SAM_BUILD_MODE`). IDE Toolkit will set the env var when calling `sam build` +while debugging. + * Pros + * Reduces cognitive load on customers that don't care about debugging dotnet apps through command line. + * Could force customers to use AWS Toolkit by default + * Cons + * Building debug artifacts becomes a hidden feature (silent contract) + * Environment Variables tend to be more set and forget. + * Need to conditionally add Env Var instead of a more convenient flag +3. Seamless Integration: `sam build` produces debug artifacts by default and `sam package` will build +non debug artifacts by default + * Pros + * Customers should never have to think about 'when to produce debug artifacts' or forget to add a flag + * Cons + * Requires additional work on package to auto build. + * Breaks what is produced from `sam build` as build is positioned to produce artifacts that are ready for deployment + +Proposal +-------- + +My recommendation is to follow Option #2 from above, mainly because: + +- Seamless Integration (#3) is best experience but is large in scope. +- CLI Option (#1) exposes a flag for what we think will be a rarely used CLI feature. We think so because manually +setting up debugging is cumbersome. Given that .NET developers generally prefer to be within the IDE, I find it hard +to believe someone will want to go out of the way of setting it up through CLI. +- Not a one-way-door. We can always add a CLI option to pair with env var later. + +`.samrc` Changes +---------------- + +*Explain the new configuration entries, if any, you want to add to +.samrc* + +Security +-------- + +*Tip: How does this change impact security? Answer the following +questions to help answer this question better:* + +**What new dependencies (libraries/cli) does this change require?** +No + +**What other Docker container images are you using?** +N/A + +**Are you creating a new HTTP endpoint? If so explain how it will be +created & used** +No + +**Are you connecting to a remote API? If so explain how is this +connection secured** +No + +**Are you reading/writing to a temporary folder? If so, what is this +used for and when do you clean up?** +No + +**How do you validate new .samrc configuration?** +N/A + +Documentation Changes +--------------------- + +Open Issues +----------- + +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/designs/sam_build_cmd.md b/designs/sam_build_cmd.md index 1f52752cb8..b4cfb86ac5 100644 --- a/designs/sam_build_cmd.md +++ b/designs/sam_build_cmd.md @@ -64,6 +64,8 @@ Success criteria for the change 8. Integrate build action with `sam local/package/deploy` commands so the Lambda functions will be automatically built as part of the command without explicitly running the build command. +9. Support for building the app for debugging locally with debug + symbols (ex: Golang) [see design doc](build_debug_artifacts.md) Out-of-Scope ------------ @@ -76,8 +78,6 @@ Out-of-Scope package manager (ex: images, css etc) 3. Support to exclude certain files from the built artifact (ex: using .gitignore or using regex) -4. Support for building the app for debugging locally with debug - symbols (ex: Golang) 5. Support caching dependencies & re-installing them only when the dependency manifest changes (ex: by maintaining hash of package.json) diff --git a/requirements/base.txt b/requirements/base.txt index 64f1880ecc..310077c683 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -12,5 +12,5 @@ dateparser~=0.7 python-dateutil~=2.6 pathlib2~=2.3.2; python_version<"3.4" requests==2.20.1 -aws_lambda_builders==0.2.1 serverlessrepo==0.1.8 +aws_lambda_builders==0.3.0 diff --git a/samcli/__init__.py b/samcli/__init__.py index e2fbb2e928..60b01efcdd 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = '0.14.2' +__version__ = '0.15.0' diff --git a/samcli/commands/build/build_context.py b/samcli/commands/build/build_context.py index 5c4dd9f947..dfed756294 100644 --- a/samcli/commands/build/build_context.py +++ b/samcli/commands/build/build_context.py @@ -26,6 +26,7 @@ def __init__(self, template_file, base_dir, build_dir, + mode, manifest_path=None, clean=False, use_container=False, @@ -42,6 +43,7 @@ def __init__(self, self._parameter_overrides = parameter_overrides self._docker_network = docker_network self._skip_pull_image = skip_pull_image + self._mode = mode self._function_provider = None self._template_dict = None @@ -73,21 +75,16 @@ def __exit__(self, *args): @staticmethod def _setup_build_dir(build_dir, clean): + build_path = pathlib.Path(build_dir) - # Get absolute path - build_dir = str(pathlib.Path(build_dir).resolve()) - - if not pathlib.Path(build_dir).exists(): - # Build directory does not exist. Create the directory and all intermediate paths - os.makedirs(build_dir, BuildContext._BUILD_DIR_PERMISSIONS) - - if os.listdir(build_dir) and clean: - # Build folder contains something inside. Clear everything. + if build_path.exists() and os.listdir(build_dir) and clean: + # build folder contains something inside. Clear everything. shutil.rmtree(build_dir) - # this would have cleared the parent folder as well. So recreate it. - os.mkdir(build_dir, BuildContext._BUILD_DIR_PERMISSIONS) - return build_dir + build_path.mkdir(mode=BuildContext._BUILD_DIR_PERMISSIONS, parents=True, exist_ok=True) + + # ensure path resolving is done after creation: https://bugs.python.org/issue32434 + return str(build_path.resolve()) @property def container_manager(self): @@ -127,3 +124,7 @@ def manifest_path_override(self): return os.path.abspath(self._manifest_path) return None + + @property + def mode(self): + return self._mode diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index a8c3660eb3..4c2df81fe8 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -11,7 +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, BuildError, UnsupportedBuilderLibraryVersionError +from samcli.lib.build.app_builder import ApplicationBuilder, BuildError, UnsupportedBuilderLibraryVersionError, \ + ContainerBuildNotSupported from samcli.lib.build.workflow_config import UnsupportedRuntimeException from samcli.commands._utils.template import move_template @@ -31,9 +32,10 @@ Supported Runtimes ------------------ 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 +2. Nodejs 8.10, 6.10 using NPM\n +3. Ruby 2.5 using Bundler\n +4. Java 8 using Gradle\n +5. Dotnetcore2.0 and 2.1 using Dotnet CLI (without --use-container flag)\n \b Examples -------- @@ -88,11 +90,14 @@ def cli(ctx, manifest, docker_network, skip_pull_image, - parameter_overrides): + parameter_overrides, + ): # All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing + mode = _get_mode_value_from_envvar("SAM_BUILD_MODE", choices=["debug"]) + do_cli(template, base_dir, build_dir, True, use_container, manifest, docker_network, - skip_pull_image, parameter_overrides) # pragma: no cover + skip_pull_image, parameter_overrides, mode) # pragma: no cover def do_cli(template, # pylint: disable=too-many-locals @@ -103,7 +108,8 @@ def do_cli(template, # pylint: disable=too-many-locals manifest_path, docker_network, skip_pull_image, - parameter_overrides): + parameter_overrides, + mode): """ Implementation of the ``cli`` method """ @@ -121,13 +127,15 @@ def do_cli(template, # pylint: disable=too-many-locals use_container=use_container, parameter_overrides=parameter_overrides, docker_network=docker_network, - skip_pull_image=skip_pull_image) as ctx: + skip_pull_image=skip_pull_image, + mode=mode) as ctx: builder = ApplicationBuilder(ctx.function_provider, ctx.build_dir, ctx.base_dir, manifest_path_override=ctx.manifest_path_override, - container_manager=ctx.container_manager + container_manager=ctx.container_manager, + mode=ctx.mode ) try: artifacts = builder.build() @@ -147,8 +155,9 @@ def do_cli(template, # pylint: disable=too-many-locals click.secho(msg, fg="yellow") - except (UnsupportedRuntimeException, BuildError, UnsupportedBuilderLibraryVersionError) as ex: - click.secho("Build Failed", fg="red") + except (UnsupportedRuntimeException, BuildError, UnsupportedBuilderLibraryVersionError, + ContainerBuildNotSupported) as ex: + click.secho("\nBuild Failed", fg="red") raise UserException(str(ex)) @@ -175,3 +184,16 @@ def gen_success_msg(artifacts_dir, output_template_path, is_default_build_dir): template=output_template_path) return msg + + +def _get_mode_value_from_envvar(name, choices): + + mode = os.environ.get(name, None) + if not mode: + return None + + if mode not in choices: + raise click.UsageError("Invalid value for 'mode': invalid choice: {}. (choose from {})" + .format(mode, choices)) + + return mode diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index 0e0ac5a93a..2ad2fe7b89 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -19,7 +19,7 @@ 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 +from .workflow_config import get_workflow_config, supports_build_in_container LOG = logging.getLogger(__name__) @@ -33,6 +33,10 @@ def __init__(self, container_name, error_msg): Exception.__init__(self, msg.format(container_name=container_name, error_msg=error_msg)) +class ContainerBuildNotSupported(Exception): + pass + + class BuildError(Exception): pass @@ -50,7 +54,8 @@ def __init__(self, base_dir, manifest_path_override=None, container_manager=None, - parallel=False): + parallel=False, + mode=None): """ Initialize the class @@ -70,6 +75,9 @@ def __init__(self, parallel : bool Optional. Set to True to build each function in parallel to improve performance + + mode : str + Optional, name of the build mode to use ex: 'debug' """ self._function_provider = function_provider self._build_dir = build_dir @@ -78,6 +86,7 @@ def __init__(self, self._container_manager = container_manager self._parallel = parallel + self._mode = mode def build(self): """ @@ -207,7 +216,8 @@ def _build_function_in_process(self, scratch_dir, manifest_path, runtime=runtime, - executable_search_paths=config.executable_search_paths) + executable_search_paths=config.executable_search_paths, + mode=self._mode) except LambdaBuilderError as ex: raise BuildError(str(ex)) @@ -221,6 +231,13 @@ def _build_function_on_container(self, # pylint: disable=too-many-locals manifest_path, runtime): + if not self._container_manager.is_docker_reachable: + raise BuildError("Docker is unreachable. Docker needs to be running to build inside a container.") + + container_build_supported, reason = supports_build_in_container(config) + if not container_build_supported: + raise ContainerBuildNotSupported(reason) + # If we are printing debug logs in SAM CLI, the builder library should also print debug logs log_level = LOG.getEffectiveLevel() @@ -234,7 +251,8 @@ def _build_function_on_container(self, # pylint: disable=too-many-locals log_level=log_level, optimizations=None, options=None, - executable_search_paths=config.executable_search_paths) + executable_search_paths=config.executable_search_paths, + mode=self._mode) try: try: diff --git a/samcli/lib/build/workflow_config.py b/samcli/lib/build/workflow_config.py index fd901290a3..28fe8e217e 100644 --- a/samcli/lib/build/workflow_config.py +++ b/samcli/lib/build/workflow_config.py @@ -41,6 +41,13 @@ manifest_name="build.gradle", executable_search_paths=None) +JAVA_KOTLIN_GRADLE_CONFIG = CONFIG( + language="java", + dependency_manager="gradle", + application_framework=None, + manifest_name="build.gradle.kts", + executable_search_paths=None) + JAVA_MAVEN_CONFIG = CONFIG( language="java", dependency_manager="maven", @@ -48,6 +55,13 @@ manifest_name="pom.xml", executable_search_paths=None) +DOTNET_CLIPACKAGE_CONFIG = CONFIG( + language="dotnet", + dependency_manager="cli-package", + application_framework=None, + manifest_name=".csproj", + executable_search_paths=None) + class UnsupportedRuntimeException(Exception): pass @@ -85,12 +99,15 @@ def get_workflow_config(runtime, code_dir, project_dir): "nodejs6.10": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs8.10": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "ruby2.5": BasicWorkflowSelector(RUBY_BUNDLER_CONFIG), + "dotnetcore2.0": BasicWorkflowSelector(DOTNET_CLIPACKAGE_CONFIG), + "dotnetcore2.1": BasicWorkflowSelector(DOTNET_CLIPACKAGE_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]), + JAVA_KOTLIN_GRADLE_CONFIG._replace(executable_search_paths=[code_dir, project_dir]), JAVA_MAVEN_CONFIG ]), } @@ -108,6 +125,42 @@ def get_workflow_config(runtime, code_dir, project_dir): .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. + + Parameters + ---------- + config namedtuple(Capability) + Config specifying the particular build workflow + + Returns + ------- + tuple(bool, str) + True, if this workflow can be built inside a container. False, along with a reason message if it cannot be. + """ + + def _key(c): + return str(c.language) + str(c.dependency_manager) + str(c.application_framework) + + # This information could have beeen bundled inside the Workflow Config object. But we this way because + # ultimately the workflow's implementation dictates whether it can run within a container or not. + # A "workflow config" is like a primary key to identify the workflow. So we use the config as a key in the + # map to identify which workflows can support building within a container. + + unsupported = { + _key(DOTNET_CLIPACKAGE_CONFIG): "We do not support building .NET Core Lambda functions within a container. " + "Try building without the container. Most .NET Core functions will build " + "successfully.", + } + + thiskey = _key(config) + if thiskey in unsupported: + return False, unsupported[thiskey] + + return True, None + + class BasicWorkflowSelector(object): """ Basic workflow selector that returns the first available configuration in the given list of configurations diff --git a/samcli/local/common/runtime_template.py b/samcli/local/common/runtime_template.py index 731410a1df..528732801b 100644 --- a/samcli/local/common/runtime_template.py +++ b/samcli/local/common/runtime_template.py @@ -49,7 +49,7 @@ "runtimes": ["dotnetcore", "dotnetcore1.0", "dotnetcore2.0", "dotnetcore2.1"], "dependency_manager": "cli-package", "init_location": os.path.join(_templates, "cookiecutter-aws-sam-hello-dotnet"), - "build": False + "build": True }, ], "go": [ diff --git a/samcli/local/docker/container.py b/samcli/local/docker/container.py index 41121b7c22..97e7a8613e 100644 --- a/samcli/local/docker/container.py +++ b/samcli/local/docker/container.py @@ -84,7 +84,7 @@ def create(self): if self.is_created(): raise RuntimeError("This container already exists. Cannot create again.") - LOG.info("Mounting %s as %s:ro inside runtime container", self._host_dir, self._working_dir) + LOG.info("Mounting %s as %s:ro,delegated inside runtime container", self._host_dir, self._working_dir) kwargs = { "command": self._cmd, @@ -95,7 +95,7 @@ def create(self): # https://docs.docker.com/storage/bind-mounts # Mount the host directory as "read only" inside container "bind": self._working_dir, - "mode": "ro" + "mode": "ro,delegated" } }, # We are not running an interactive shell here. diff --git a/samcli/local/docker/lambda_build_container.py b/samcli/local/docker/lambda_build_container.py index b7ce5ff60d..b1e3d23077 100644 --- a/samcli/local/docker/lambda_build_container.py +++ b/samcli/local/docker/lambda_build_container.py @@ -36,7 +36,8 @@ def __init__(self, # pylint: disable=too-many-locals optimizations=None, options=None, executable_search_paths=None, - log_level=None): + log_level=None, + mode=None): abs_manifest_path = pathlib.Path(manifest_path).resolve() manifest_file_name = abs_manifest_path.name @@ -67,7 +68,8 @@ def __init__(self, # pylint: disable=too-many-locals runtime, optimizations, options, - executable_search_paths) + executable_search_paths, + mode) image = LambdaBuildContainer._get_image(runtime) entry = LambdaBuildContainer._get_entrypoint(request_json) @@ -111,7 +113,8 @@ def _make_request(protocol_version, runtime, optimizations, options, - executable_search_paths): + executable_search_paths, + mode): return json.dumps({ "jsonschema": "2.0", @@ -134,7 +137,8 @@ def _make_request(protocol_version, "runtime": runtime, "optimizations": optimizations, "options": options, - "executable_search_paths": executable_search_paths + "executable_search_paths": executable_search_paths, + "mode": mode } }) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/tests/test_cookiecutter.py b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/tests/test_cookiecutter.py index 285b06b26c..987385e1fd 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/tests/test_cookiecutter.py +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/tests/test_cookiecutter.py @@ -19,7 +19,7 @@ def test_project_tree(cookies): assert result.project.join('src', 'HelloWorld').isdir() assert result.project.join( 'src', 'HelloWorld', 'HelloWorld.csproj').isfile() - assert result.project.join('src', 'HelloWorld', 'Program.cs').isfile() + assert result.project.join('src', 'HelloWorld', 'Function.cs').isfile() assert result.project.join( 'src', 'HelloWorld', 'aws-lambda-tools-defaults.json').isfile() assert result.project.join( @@ -30,7 +30,7 @@ def test_project_tree(cookies): def test_app_content(cookies): result = cookies.bake(extra_context={'project_name': 'my_lambda'}) - app_file = result.project.join('src', 'HelloWorld', 'Program.cs') + app_file = result.project.join('src', 'HelloWorld', 'Function.cs') app_content = app_file.readlines() app_content = ''.join(app_content) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/README.md index fd8a839788..b178a16dbc 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/README.md @@ -16,24 +16,44 @@ Please see the [currently supported patch of each major version of .NET Core](ht * [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/) * [AWS Extensions for .NET CLI](https://github.com/aws/aws-extensions-for-dotnet-cli) which are AWS extensions to the .NET CLI focused on building .NET Core and ASP.NET Core applications and deploying them to AWS services including Amazon Elastic Container Service, AWS Elastic Beanstalk and AWS Lambda. -> **Note: this project uses [Cake Build](https://cakebuild.net/) for build, test and packaging requirements. You do not need to have the [AWS Extensions for .NET CLI](https://github.com/aws/aws-extensions-for-dotnet-cli) installed, but are free to do so if you which to use them. Version 3 of the Amazon.Lambda.Tools does require .NET Core 2.1 for installation, but can be used to deploy older versions of .NET Core.** +> **Note: You do not need to have the [AWS Extensions for .NET CLI](https://github.com/aws/aws-extensions-for-dotnet-cli) installed, but are free to do so if you which to use them. Version 3 of the Amazon.Lambda.Tools does require .NET Core 2.1 for installation, but can be used to deploy older versions of .NET Core.** ## Setup process -### Linux & macOS +### Folder Structure -```bash -sh build.sh --target=Package +AWS Lambda C# runtime requires a flat folder with all dependencies including the application. SAM will use `CodeUri` property to know where to look up for both application and dependencies. `CodeUri` must be set to the path to folder containing your Lambda function source code and `.csproj` file. + +```yaml +... + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./src/HelloWorld + ... ``` -### Windows (Powershell) +### Building your application -```powershell -build.ps1 --target=Package +```bash +sam build ``` ### Local development +**Invoking function locally** + +```bash +sam local invoke --no-event +``` + +To invoke with an event you can pass in a json file to the command. + +```bash +sam local invoke -e event.json +``` + + **Invoking function locally through local API Gateway** ```bash @@ -56,17 +76,6 @@ If the previous command run successfully you should now be able to hit the follo ## Packaging and deployment -AWS Lambda C# runtime requires a flat folder with all dependencies including the application. SAM will use `CodeUri` property to know where to look up for both application and dependencies: - -```yaml -... - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: artifacts/HelloWorld.zip - ... -``` - First and foremost, we need an `S3 bucket` where we can upload our Lambda functions packaged as ZIP before we deploy anything - If you don't have a S3 bucket to store code artifacts then this is a good time to create one: ```bash @@ -108,43 +117,7 @@ For testing our code, we use XUnit and you can use `dotnet test` to run tests de dotnet test test/HelloWorld.Test ``` -Alternatively, you can use Cake. It discovers and executes all the tests. - -### Linux & macOS - -```bash -sh build.sh --target=Test -``` - -### Windows (Powershell) - -```powershell -build.ps1 --target=Test -``` - -# Appendix - -## AWS CLI commands - -AWS CLI commands to package, deploy and describe outputs defined within the AWS CloudFormation stack: - -```bash -aws cloudformation package \ - --template-file template.yaml \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME - -aws cloudformation deploy \ - --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --capabilities CAPABILITY_IAM \ - --parameter-overrides MyParameterSample=MySampleValue - -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} --query 'Stacks[].Outputs' -``` - -## Next Steps +# Next Steps Create your own .NET Core solution template to use with SAM CLI. [Cookiecutter for AWS SAM and .NET](https://github.com/aws-samples/cookiecutter-aws-sam-dotnet) provides you with a sample implementation how to use cookiecutter templating library to standardise how you initialise your Serverless projects. diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/Solution.sln b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/Solution.sln deleted file mode 100644 index 2bbfdaa95e..0000000000 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/Solution.sln +++ /dev/null @@ -1,43 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{94965890-D3F6-4FF4-8EA1-73F73E89D19C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{D8B988DB-650C-4F45-9A9A-528F9E0CDDB7}" -ProjectSection(SolutionItems) = preProject - README.md = README.md -EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{68DF9E4E-E53E-4A13-8EDB-B63691A92CA9}" -ProjectSection(SolutionItems) = preProject - build.cake = build.cake - build.ps1 = build.ps1 - build.sh = build.sh - template.yaml = template.yaml -EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6DB7C3C2-84DC-4DD5-A070-8E3F186E184F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorld", "src\HelloWorld\HelloWorld.csproj", "{9F62886F-73E6-4CBC-9A90-48BA8B56F13B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorld.Tests", "test\HelloWorld.Test\HelloWorld.Tests.csproj", "{6B28DD0E-5B35-4F5A-943A-2C80953B7B3F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {9F62886F-73E6-4CBC-9A90-48BA8B56F13B} = {94965890-D3F6-4FF4-8EA1-73F73E89D19C} - {6B28DD0E-5B35-4F5A-943A-2C80953B7B3F} = {6DB7C3C2-84DC-4DD5-A070-8E3F186E184F} - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9F62886F-73E6-4CBC-9A90-48BA8B56F13B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F62886F-73E6-4CBC-9A90-48BA8B56F13B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F62886F-73E6-4CBC-9A90-48BA8B56F13B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F62886F-73E6-4CBC-9A90-48BA8B56F13B}.Release|Any CPU.Build.0 = Release|Any CPU - {6B28DD0E-5B35-4F5A-943A-2C80953B7B3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B28DD0E-5B35-4F5A-943A-2C80953B7B3F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B28DD0E-5B35-4F5A-943A-2C80953B7B3F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B28DD0E-5B35-4F5A-943A-2C80953B7B3F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.cake b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.cake deleted file mode 100644 index 8a9fdb165e..0000000000 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.cake +++ /dev/null @@ -1,261 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// ARGUMENTS -/////////////////////////////////////////////////////////////////////////////// - -var target = Argument("target", "Default"); -var configuration = Argument("configuration", "Release"); -var verbosity = Argument("verbosity", "Minimal"); - -/////////////////////////////////////////////////////////////////////////////// -// GLOBAL VARIABLES -/////////////////////////////////////////////////////////////////////////////// - -var sourceDir = Directory("./src"); - -var solutions = GetFiles("./**/*.sln"); -var projects = new [] -{ - sourceDir.Path + "/HelloWorld/HelloWorld.csproj", -}; - -// BUILD OUTPUT DIRECTORIES -var artifactsDir = Directory("./artifacts"); -var publishDir = Directory("./publish/"); - -// VERBOSITY -var dotNetCoreVerbosity = Cake.Common.Tools.DotNetCore.DotNetCoreVerbosity.Normal; -if (!Enum.TryParse(verbosity, true, out dotNetCoreVerbosity)) -{ - dotNetCoreVerbosity = Cake.Common.Tools.DotNetCore.DotNetCoreVerbosity.Normal; - Warning( - "Verbosity could not be parsed into type 'Cake.Common.Tools.DotNetCore.DotNetCoreVerbosity'. Defaulting to {0}", - dotNetCoreVerbosity); -} - -/////////////////////////////////////////////////////////////////////////////// -// COMMON FUNCTION DEFINITIONS -/////////////////////////////////////////////////////////////////////////////// - -string GetProjectName(string project) -{ - return project - .Split(new [] {'/'}, StringSplitOptions.RemoveEmptyEntries) - .Last() - .Replace(".csproj", string.Empty); -} - -/////////////////////////////////////////////////////////////////////////////// -// SETUP / TEARDOWN -/////////////////////////////////////////////////////////////////////////////// - -Setup(ctx => -{ - // Executed BEFORE the first task. - EnsureDirectoryExists(artifactsDir); - EnsureDirectoryExists(publishDir); - Information("Running tasks..."); -}); - -Teardown(ctx => -{ - // Executed AFTER the last task. - Information("Finished running tasks."); -}); - -/////////////////////////////////////////////////////////////////////////////// -// TASK DEFINITIONS -/////////////////////////////////////////////////////////////////////////////// - -Task("Clean") - .Description("Cleans all directories that are used during the build process.") - .Does(() => - { - foreach(var solution in solutions) - { - Information("Cleaning {0}", solution.FullPath); - CleanDirectories(solution.FullPath + "/**/bin/" + configuration); - CleanDirectories(solution.FullPath + "/**/obj/" + configuration); - Information("{0} was clean.", solution.FullPath); - } - - CleanDirectory(artifactsDir); - CleanDirectory(publishDir); - }); - -Task("Restore") - .Description("Restores all the NuGet packages that are used by the specified solution.") - .Does(() => - { - var settings = new DotNetCoreRestoreSettings - { - DisableParallel = false, - NoCache = true, - Verbosity = dotNetCoreVerbosity - }; - - foreach(var solution in solutions) - { - Information("Restoring NuGet packages for '{0}'...", solution); - DotNetCoreRestore(solution.FullPath, settings); - Information("NuGet packages restored for '{0}'.", solution); - } - }); - -Task("Build") - .Description("Builds all the different parts of the project.") - .Does(() => - { - var msBuildSettings = new DotNetCoreMSBuildSettings - { - TreatAllWarningsAs = MSBuildTreatAllWarningsAs.Error, - Verbosity = dotNetCoreVerbosity - }; - - var settings = new DotNetCoreBuildSettings - { - Configuration = configuration, - MSBuildSettings = msBuildSettings, - NoRestore = true - }; - - foreach(var solution in solutions) - { - Information("Building '{0}'...", solution); - DotNetCoreBuild(solution.FullPath, settings); - Information("'{0}' has been built.", solution); - } - }); - -Task("Test-Unit") - .Description("Tests all the different parts of the project.") - .Does(() => - { - var settings = new DotNetCoreTestSettings - { - Configuration = configuration, - NoRestore = true, - NoBuild = true - }; - - var projectFiles = GetFiles("./test/**/*.csproj"); - foreach(var file in projectFiles) - { - Information("Testing '{0}'...", file); - DotNetCoreTest(file.FullPath, settings); - Information("'{0}' has been tested.", file); - } - }); - -Task("Publish") - .Description("Publish the Lambda Functions.") - .Does(() => - { - foreach(var project in projects) - { - var projectName = project - .Split(new [] {'/'}, StringSplitOptions.RemoveEmptyEntries) - .Last() - .Replace(".csproj", string.Empty); - - var outputDirectory = System.IO.Path.Combine(publishDir, projectName); - - var msBuildSettings = new DotNetCoreMSBuildSettings - { - TreatAllWarningsAs = MSBuildTreatAllWarningsAs.Error, - Verbosity = dotNetCoreVerbosity - }; - - var settings = new DotNetCorePublishSettings - { - Configuration = configuration, - MSBuildSettings = msBuildSettings, - NoRestore = true, - OutputDirectory = outputDirectory, - Verbosity = dotNetCoreVerbosity - }; - - Information("Publishing '{0}'...", projectName); - DotNetCorePublish(project, settings); - Information("'{0}' has been published.", projectName); - } - }); - -Task("Pack") - .Description("Packs all the different parts of the project.") - .Does(() => - { - foreach(var project in projects) - { - var projectName = GetProjectName(project); - - Information("Packing '{0}'...", projectName); - var path = System.IO.Path.Combine(publishDir, projectName); - var files = GetFiles(path + "/*.*"); - Zip( - path, - System.IO.Path.Combine(artifactsDir, $"{projectName}.zip"), - files); - Information("'{0}' has been packed.", projectName); - } - }); - -Task("Run-Local") - .Description("Runs all the acceptance tests locally.") - .Does(() => - { - var settings = new ProcessSettings - { - Arguments = "local invoke \"HelloWorld\" -e event.json", - }; - - Information("Starting the SAM local..."); - using(var process = StartAndReturnProcess("sam", settings)) - { - process.WaitForExit(); - Information("Exit code: {0}", process.GetExitCode()); - } - Information("SAM local has finished."); - }); - -/////////////////////////////////////////////////////////////////////////////// -// TARGETS -/////////////////////////////////////////////////////////////////////////////// - -Task("Package") - .Description("This is the task which will run if target Package is passed in.") - .IsDependentOn("Clean") - .IsDependentOn("Restore") - .IsDependentOn("Build") - .IsDependentOn("Test-Unit") - .IsDependentOn("Publish") - .IsDependentOn("Pack") - .Does(() => { Information("Package target ran."); }); - -Task("Test") - .Description("This is the task which will run if target Test is passed in.") - .IsDependentOn("Clean") - .IsDependentOn("Restore") - .IsDependentOn("Build") - .IsDependentOn("Test-Unit") - .Does(() => { Information("Test target ran."); }); - -Task("Run") - .Description("This is the task which will run if target Run is passed in.") - .IsDependentOn("Clean") - .IsDependentOn("Restore") - .IsDependentOn("Build") - .IsDependentOn("Test-Unit") - .IsDependentOn("Publish") - .IsDependentOn("Pack") - .IsDependentOn("Run-Local") - .Does(() => { Information("Run target ran."); }); - -Task("Default") - .Description("This is the default task which will run if no specific target is passed in.") - .IsDependentOn("Package"); - -/////////////////////////////////////////////////////////////////////////////// -// EXECUTION -/////////////////////////////////////////////////////////////////////////////// - -RunTarget(target); diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.ps1 b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.ps1 deleted file mode 100644 index 82529cf602..0000000000 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.ps1 +++ /dev/null @@ -1,235 +0,0 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -<# - -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. - -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. - -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER ShowDescription -Shows description about tasks. -.PARAMETER DryRun -Performs a dry run. -.PARAMETER Experimental -Uses the nightly builds of the Roslyn script engine. -.PARAMETER Mono -Uses the Mono Compiler rather than the Roslyn script engine. -.PARAMETER SkipToolPackageRestore -Skips restoring of packages. -.PARAMETER ScriptArgs -Remaining arguments are added here. - -.LINK -https://cakebuild.net - -#> - -[CmdletBinding()] -Param( - [string]$Script = "build.cake", - [string]$Target, - [string]$Configuration, - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity, - [switch]$ShowDescription, - [Alias("WhatIf", "Noop")] - [switch]$DryRun, - [switch]$Experimental, - [switch]$Mono, - [switch]$SkipToolPackageRestore, - [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs -) - -[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null -function MD5HashFile([string] $filePath) -{ - if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) - { - return $null - } - - [System.IO.Stream] $file = $null; - [System.Security.Cryptography.MD5] $md5 = $null; - try - { - $md5 = [System.Security.Cryptography.MD5]::Create() - $file = [System.IO.File]::OpenRead($filePath) - return [System.BitConverter]::ToString($md5.ComputeHash($file)) - } - finally - { - if ($file -ne $null) - { - $file.Dispose() - } - } -} - -function GetProxyEnabledWebClient -{ - $wc = New-Object System.Net.WebClient - $proxy = [System.Net.WebRequest]::GetSystemWebProxy() - $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials - $wc.Proxy = $proxy - return $wc -} - -Write-Host "Preparing to run build script..." - -if(!$PSScriptRoot){ - $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -} - -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" -$MODULES_DIR = Join-Path $TOOLS_DIR "Modules" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" -$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" -$ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" -$MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" - -# Make sure tools folder exists -if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type directory | out-null -} - -# Make sure that packages.config exist. -if (!(Test-Path $PACKAGES_CONFIG)) { - Write-Verbose -Message "Downloading packages.config..." - try { - $wc = GetProxyEnabledWebClient - $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { - Throw "Could not download packages.config." - } -} - -# Try find NuGet.exe in path if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Trying to find nuget.exe in PATH..." - $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } - $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 - if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { - Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." - $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName - } -} - -# Try download NuGet.exe if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Downloading NuGet.exe..." - try { - $wc = GetProxyEnabledWebClient - $wc.DownloadFile($NUGET_URL, $NUGET_EXE) - } catch { - Throw "Could not download NuGet.exe." - } -} - -# Save nuget.exe path to environment to be available to child processed -$ENV:NUGET_EXE = $NUGET_EXE - -# Restore tools from NuGet? -if(-Not $SkipToolPackageRestore.IsPresent) { - Push-Location - Set-Location $TOOLS_DIR - - # Check for changes in packages.config and remove installed tools if true. - [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) - if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or - ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { - Write-Verbose -Message "Missing or changed package.config hash..." - Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | - Remove-Item -Recurse - } - - Write-Verbose -Message "Restoring tools from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet tools." - } - else - { - $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" - } - Write-Verbose -Message ($NuGetOutput | out-string) - - Pop-Location -} - -# Restore addins from NuGet -if (Test-Path $ADDINS_PACKAGES_CONFIG) { - Push-Location - Set-Location $ADDINS_DIR - - Write-Verbose -Message "Restoring addins from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet addins." - } - - Write-Verbose -Message ($NuGetOutput | out-string) - - Pop-Location -} - -# Restore modules from NuGet -if (Test-Path $MODULES_PACKAGES_CONFIG) { - Push-Location - Set-Location $MODULES_DIR - - Write-Verbose -Message "Restoring modules from NuGet..." - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet modules." - } - - Write-Verbose -Message ($NuGetOutput | out-string) - - Pop-Location -} - -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe at $CAKE_EXE" -} - - - -# Build Cake arguments -$cakeArguments = @("$Script"); -if ($Target) { $cakeArguments += "-target=$Target" } -if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } -if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } -if ($ShowDescription) { $cakeArguments += "-showdescription" } -if ($DryRun) { $cakeArguments += "-dryrun" } -if ($Experimental) { $cakeArguments += "-experimental" } -if ($Mono) { $cakeArguments += "-mono" } -$cakeArguments += $ScriptArgs - -# Start Cake -Write-Host "Running build script..." -&$CAKE_EXE $cakeArguments -exit $LASTEXITCODE diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.sh b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.sh deleted file mode 100644 index b9e12527f1..0000000000 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/build.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash - -########################################################################## -# This is the Cake bootstrapper script for Linux and OS X. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -# Define directories. -SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -TOOLS_DIR=$SCRIPT_DIR/tools -ADDINS_DIR=$TOOLS_DIR/Addins -MODULES_DIR=$TOOLS_DIR/Modules -NUGET_EXE=$TOOLS_DIR/nuget.exe -CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe -PACKAGES_CONFIG=$TOOLS_DIR/packages.config -PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum -ADDINS_PACKAGES_CONFIG=$ADDINS_DIR/packages.config -MODULES_PACKAGES_CONFIG=$MODULES_DIR/packages.config - -# Define md5sum or md5 depending on Linux/OSX -MD5_EXE= -if [[ "$(uname -s)" == "Darwin" ]]; then - MD5_EXE="md5 -r" -else - MD5_EXE="md5sum" -fi - -# Define default arguments. -SCRIPT="build.cake" -CAKE_ARGUMENTS=() - -# Parse arguments. -for i in "$@"; do - case $1 in - -s|--script) SCRIPT="$2"; shift ;; - --) shift; CAKE_ARGUMENTS+=("$@"); break ;; - *) CAKE_ARGUMENTS+=("$1") ;; - esac - shift -done - -# Make sure the tools folder exist. -if [ ! -d "$TOOLS_DIR" ]; then - mkdir "$TOOLS_DIR" -fi - -# Make sure that packages.config exist. -if [ ! -f "$TOOLS_DIR/packages.config" ]; then - echo "Downloading packages.config..." - curl -Lsfo "$TOOLS_DIR/packages.config" https://cakebuild.net/download/bootstrapper/packages - if [ $? -ne 0 ]; then - echo "An error occurred while downloading packages.config." - exit 1 - fi -fi - -# Download NuGet if it does not exist. -if [ ! -f "$NUGET_EXE" ]; then - echo "Downloading NuGet..." - curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe - if [ $? -ne 0 ]; then - echo "An error occurred while downloading nuget.exe." - exit 1 - fi -fi - -# Restore tools from NuGet. -pushd "$TOOLS_DIR" >/dev/null -if [ ! -f "$PACKAGES_CONFIG_MD5" ] || [ "$( cat "$PACKAGES_CONFIG_MD5" | sed 's/\r$//' )" != "$( $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' )" ]; then - find . -type d ! -name . ! -name 'Cake.Bakery' | xargs rm -rf -fi - -mono "$NUGET_EXE" install -ExcludeVersion -if [ $? -ne 0 ]; then - echo "Could not restore NuGet tools." - exit 1 -fi - -$MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' >| "$PACKAGES_CONFIG_MD5" - -popd >/dev/null - -# Restore addins from NuGet. -if [ -f "$ADDINS_PACKAGES_CONFIG" ]; then - pushd "$ADDINS_DIR" >/dev/null - - mono "$NUGET_EXE" install -ExcludeVersion - if [ $? -ne 0 ]; then - echo "Could not restore NuGet addins." - exit 1 - fi - - popd >/dev/null -fi - -# Restore modules from NuGet. -if [ -f "$MODULES_PACKAGES_CONFIG" ]; then - pushd "$MODULES_DIR" >/dev/null - - mono "$NUGET_EXE" install -ExcludeVersion - if [ $? -ne 0 ]; then - echo "Could not restore NuGet modules." - exit 1 - fi - - popd >/dev/null -fi - -# Make sure that Cake has been installed. -if [ ! -f "$CAKE_EXE" ]; then - echo "Could not find Cake.exe at '$CAKE_EXE'." - exit 1 -fi - -# Start Cake -exec mono "$CAKE_EXE" $SCRIPT "${CAKE_ARGUMENTS[@]}" diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/omnisharp.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/omnisharp.json new file mode 100644 index 0000000000..c42f8db91c --- /dev/null +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/omnisharp.json @@ -0,0 +1,11 @@ +{ + "fileOptions": { + "excludeSearchPatterns": [ + "**/bin/**/*", + "**/obj/**/*" + ] + }, + "msbuild": { + "Platform": "rhel.7.2-x64" + } +} \ No newline at end of file diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/Program.cs b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/Function.cs similarity index 100% rename from samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/Program.cs rename to samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/Function.cs diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/HelloWorld.csproj b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/HelloWorld.csproj index bd6e37009c..bce2dcc23f 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/HelloWorld.csproj +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/HelloWorld.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/aws-lambda-tools-defaults.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/aws-lambda-tools-defaults.json new file mode 100644 index 0000000000..4e71062820 --- /dev/null +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/aws-lambda-tools-defaults.json @@ -0,0 +1,23 @@ +{ + "Information" : [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + + "dotnet lambda help", + + "All the command line options for the Lambda command can be specified in this file." + ], + + "profile":"", + "region" : "", + "configuration": "Release", + {%- if cookiecutter.runtime == 'dotnetcore2.0' %} + "framework": "netcoreapp2.0", + {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} + "framework" : "netcoreapp2.1", + {%- endif %} + "function-runtime":"{{ cookiecutter.runtime }}", + "function-memory-size" : 256, + "function-timeout" : 30, + "function-handler" : "HelloWorld::HelloWorld.Function::FunctionHandler" +} 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 f6367f0594..7726bb86a9 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 @@ -12,11 +12,11 @@ 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 + CodeUri: ./src/HelloWorld/ Handler: HelloWorld::HelloWorld.Function::FunctionHandler {%- if cookiecutter.runtime == 'dotnetcore2.0' %} Runtime: {{ cookiecutter.runtime }} - {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} + {%- else %} 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 diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/FunctionTest.cs b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/FunctionTest.cs index 96881f0c67..83c5801060 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/FunctionTest.cs +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/FunctionTest.cs @@ -62,4 +62,4 @@ public void TestHelloWorldFunctionHandler() Assert.Equal(ExpectedResponse.StatusCode, response.StatusCode); } } -} +} \ No newline at end of file diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/HelloWorld.Tests.csproj b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/HelloWorld.Tests.csproj index 0d6699418c..50148be3a2 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/HelloWorld.Tests.csproj +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/test/HelloWorld.Test/HelloWorld.Tests.csproj @@ -1,26 +1,24 @@ - - - - {%- if cookiecutter.runtime == 'dotnetcore2.0' %} - netcoreapp2.0 - {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} - netcoreapp2.1 - {%- endif %} - - - - - - - - - - - - - - - - - - + + + + {%- if cookiecutter.runtime == 'dotnetcore2.0' %} + netcoreapp2.0 + {%- elif cookiecutter.runtime == 'dotnetcore2.1' or cookiecutter.runtime == 'dotnet' %} + netcoreapp2.1 + {%- endif %} + + + + + + + + + + + + + + + + diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/app.rb b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/app.rb index ed241af8fa..e2a102f70c 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/app.rb +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/app.rb @@ -12,7 +12,7 @@ def lambda_handler(event:, context:) # context: object, required # Lambda Context runtime methods and attributes - # Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + # Context doc: https://docs.aws.amazon.com/lambda/latest/dg/ruby-context.html # Returns # ------ diff --git a/snap/local/launchers/launcher.sh b/snap/local/launchers/launcher.sh new file mode 100755 index 0000000000..764b7f8268 --- /dev/null +++ b/snap/local/launchers/launcher.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +HOME="$(getent passwd "${USER}" | cut -d: -f6)" + +exec "${@}" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000000..2ab2e0a434 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,50 @@ +name: aws-sam-cli +version: git +summary: The AWS SAM CLI tool for the AWS Serverless Application Model +description: > + `sam` is the AWS CLI tool for managing Serverless applications written + with the AWS Serverless Application Model (SAM). + SAM CLI can be used to test functions locally, start a local API Gateway + from a SAM template, validate a SAM template, fetch logs, generate sample + payloads for various event sources, and generate a SAM project in your + favorite Lambda Runtime. + +license: Apache-2.0 + +base: core18 + +grade: stable +confinement: strict + +plugs: + personal-files: + read: + - $HOME/.aws + +parts: + launchers: + plugin: dump + source: snap/local/launchers + organize: + "*": bin/ + + aws-sam-cli: + plugin: python + python-version: python3 + python-packages: + - awscli + requirements: + - requirements/base.txt + source: . + +apps: + aws-sam-cli: + adapter: full + command-chain: + - bin/launcher.sh + command: bin/sam + #plugs: + # - home + environment: + LC_ALL: C.UTF-8 + LANG: C.UTF-8 diff --git a/tests/functional/init/test_generate_project.py b/tests/functional/init/test_generate_project.py index 1c6e8bb3a6..f7be76f731 100644 --- a/tests/functional/init/test_generate_project.py +++ b/tests/functional/init/test_generate_project.py @@ -12,6 +12,7 @@ class TestCli(TestCase): def setUp(self): self.location = None self.runtime = "python3.6" + self.dependency_manager = "pip" self.output_dir = tempfile.mkdtemp() self.name = "testing project {}".format(random.randint(1, 10)) self.no_input = False @@ -32,7 +33,8 @@ def test_generate_project(self): # GIVEN generate_project successfully created a project # WHEN a project name has been passed init_cli( - ctx=None, location=self.location, runtime=self.runtime, output_dir=self.output_dir, + ctx=None, location=self.location, runtime=self.runtime, + dependency_manager=self.dependency_manager, output_dir=self.output_dir, name=self.name, no_input=self.no_input) # THEN we should see a new project created and a successful return @@ -45,7 +47,8 @@ def test_custom_location(self): self.location = "https://github.com/aws-samples/cookiecutter-aws-sam-python" init_cli( - ctx=None, location=self.location, runtime=self.runtime, output_dir=self.output_dir, + ctx=None, location=self.location, runtime=self.runtime, + dependency_manager=self.dependency_manager, output_dir=self.output_dir, name=self.name, no_input=True) # THEN we should see a new project created and a successful return diff --git a/tests/integration/buildcmd/build_integ_base.py b/tests/integration/buildcmd/build_integ_base.py index a40569455a..5dd24e1d7c 100644 --- a/tests/integration/buildcmd/build_integ_base.py +++ b/tests/integration/buildcmd/build_integ_base.py @@ -61,7 +61,7 @@ def base_command(cls): return command def get_command_list(self, build_dir=None, base_dir=None, manifest_path=None, use_container=None, - parameter_overrides=None): + parameter_overrides=None, mode=None): command_list = [self.cmd, "build", "-t", self.template_path] @@ -100,7 +100,7 @@ def _verify_resource_property(self, template_path, logical_id, property, expecte 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) + LOG.info("Invoking built function '{}'".format(function_logical_id)) cmdlist = [self.cmd, "local", "invoke", function_logical_id, "-t", str(template_path), "--no-event", "--parameter-overrides", overrides] diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 3816e2624e..a1856b3f13 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -249,14 +249,17 @@ class TestBuildCommand_Java(BuildIntegBase): FUNCTION_LOGICAL_ID = "Function" USING_GRADLE_PATH = os.path.join("Java", "gradle") USING_GRADLEW_PATH = os.path.join("Java", "gradlew") + USING_GRADLE_KOTLIN_PATH = os.path.join("Java", "gradle-kotlin") USING_MAVEN_PATH = str(Path('Java', 'maven')) @parameterized.expand([ ("java8", USING_GRADLE_PATH, EXPECTED_FILES_PROJECT_MANIFEST_GRADLE, False), ("java8", USING_GRADLEW_PATH, EXPECTED_FILES_PROJECT_MANIFEST_GRADLE, False), + ("java8", USING_GRADLE_KOTLIN_PATH, EXPECTED_FILES_PROJECT_MANIFEST_GRADLE, False), ("java8", USING_MAVEN_PATH, EXPECTED_FILES_PROJECT_MANIFEST_MAVEN, False), ("java8", USING_GRADLE_PATH, EXPECTED_FILES_PROJECT_MANIFEST_GRADLE, "use_container"), ("java8", USING_GRADLEW_PATH, EXPECTED_FILES_PROJECT_MANIFEST_GRADLE, "use_container"), + ("java8", USING_GRADLE_KOTLIN_PATH, EXPECTED_FILES_PROJECT_MANIFEST_GRADLE, "use_container"), ("java8", USING_MAVEN_PATH, EXPECTED_FILES_PROJECT_MANIFEST_MAVEN, "use_container") ]) def test_with_building_java(self, runtime, code_path, expected_files, use_container): @@ -310,3 +313,92 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files, lib_dir_contents = set(os.listdir(str(resource_artifact_dir.joinpath("lib")))) self.assertEquals(lib_dir_contents, expected_modules) + + +class TestBuildCommand_Dotnet_cli_package(BuildIntegBase): + + FUNCTION_LOGICAL_ID = "Function" + EXPECTED_FILES_PROJECT_MANIFEST = {"Amazon.Lambda.APIGatewayEvents.dll", "HelloWorld.pdb", + "Amazon.Lambda.Core.dll", "HelloWorld.runtimeconfig.json", + "Amazon.Lambda.Serialization.Json.dll", "Newtonsoft.Json.dll", + "HelloWorld.deps.json", "HelloWorld.dll"} + + @parameterized.expand([ + ("dotnetcore2.0", "Dotnetcore2.0", None), + ("dotnetcore2.1", "Dotnetcore2.1", None), + ("dotnetcore2.0", "Dotnetcore2.0", "debug"), + ("dotnetcore2.1", "Dotnetcore2.1", "debug"), + ]) + def test_with_dotnetcore(self, runtime, code_uri, mode): + overrides = {"Runtime": runtime, "CodeUri": code_uri, + "Handler": "HelloWorld::HelloWorld.Function::FunctionHandler"} + cmdlist = self.get_command_list(use_container=False, + parameter_overrides=overrides) + + LOG.info("Running Command: {}".format(cmdlist)) + LOG.info("Running with SAM_BUILD_MODE={}".format(mode)) + + newenv = os.environ.copy() + if mode: + newenv["SAM_BUILD_MODE"] = mode + + process = subprocess.Popen(cmdlist, cwd=self.working_dir, env=newenv) + process.wait() + + self._verify_built_artifact(self.default_build_dir, self.FUNCTION_LOGICAL_ID, + self.EXPECTED_FILES_PROJECT_MANIFEST) + + 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 = "{'message': '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) + + @parameterized.expand([ + ("dotnetcore2.0", "Dotnetcore2.0"), + ("dotnetcore2.1", "Dotnetcore2.1"), + ]) + def test_must_fail_with_container(self, runtime, code_uri): + use_container = True + overrides = {"Runtime": runtime, "CodeUri": code_uri, + "Handler": "HelloWorld::HelloWorld.Function::FunctionHandler"} + 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() + + # Must error out, because container builds are not supported + self.assertEquals(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)) + 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) diff --git a/tests/integration/testdata/buildcmd/Dotnetcore2.0/HelloWorld.csproj b/tests/integration/testdata/buildcmd/Dotnetcore2.0/HelloWorld.csproj new file mode 100644 index 0000000000..51357eac13 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Dotnetcore2.0/HelloWorld.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp2.0 + true + + + + + + + + + + + diff --git a/tests/integration/testdata/buildcmd/Dotnetcore2.0/Program.cs b/tests/integration/testdata/buildcmd/Dotnetcore2.0/Program.cs new file mode 100644 index 0000000000..bdc1092e6e --- /dev/null +++ b/tests/integration/testdata/buildcmd/Dotnetcore2.0/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Net.Http; +using System.Net.Http.Headers; +using Newtonsoft.Json; + +using Amazon.Lambda.Core; +using Amazon.Lambda.APIGatewayEvents; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))] + +namespace HelloWorld +{ + + public class Function + { + + public string FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + return "{'message': 'Hello World'}"; + } + } +} diff --git a/tests/integration/testdata/buildcmd/Dotnetcore2.0/aws-lambda-tools-defaults.json b/tests/integration/testdata/buildcmd/Dotnetcore2.0/aws-lambda-tools-defaults.json new file mode 100644 index 0000000000..3e6486b9fc --- /dev/null +++ b/tests/integration/testdata/buildcmd/Dotnetcore2.0/aws-lambda-tools-defaults.json @@ -0,0 +1,19 @@ +{ + "Information" : [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + + "dotnet lambda help", + + "All the command line options for the Lambda command can be specified in this file." + ], + + "profile":"", + "region" : "", + "configuration": "Release", + "framework": "netcoreapp2.0", + "function-runtime":"dotnetcore2.0", + "function-memory-size" : 256, + "function-timeout" : 30, + "function-handler" : "HelloWorld::HelloWorld.Function::FunctionHandler" +} diff --git a/tests/integration/testdata/buildcmd/Dotnetcore2.1/HelloWorld.csproj b/tests/integration/testdata/buildcmd/Dotnetcore2.1/HelloWorld.csproj new file mode 100644 index 0000000000..dcc8fd0d20 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Dotnetcore2.1/HelloWorld.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp2.1 + true + + + + + + + + + + + diff --git a/tests/integration/testdata/buildcmd/Dotnetcore2.1/Program.cs b/tests/integration/testdata/buildcmd/Dotnetcore2.1/Program.cs new file mode 100644 index 0000000000..bdc1092e6e --- /dev/null +++ b/tests/integration/testdata/buildcmd/Dotnetcore2.1/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Net.Http; +using System.Net.Http.Headers; +using Newtonsoft.Json; + +using Amazon.Lambda.Core; +using Amazon.Lambda.APIGatewayEvents; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))] + +namespace HelloWorld +{ + + public class Function + { + + public string FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + return "{'message': 'Hello World'}"; + } + } +} diff --git a/tests/integration/testdata/buildcmd/Dotnetcore2.1/aws-lambda-tools-defaults.json b/tests/integration/testdata/buildcmd/Dotnetcore2.1/aws-lambda-tools-defaults.json new file mode 100644 index 0000000000..faee710ee6 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Dotnetcore2.1/aws-lambda-tools-defaults.json @@ -0,0 +1,19 @@ +{ + "Information" : [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + + "dotnet lambda help", + + "All the command line options for the Lambda command can be specified in this file." + ], + + "profile":"", + "region" : "", + "configuration": "Release", + "framework": "netcoreapp2.1", + "function-runtime":"dotnetcore2.1", + "function-memory-size" : 256, + "function-timeout" : 30, + "function-handler" : "HelloWorld::HelloWorld.Function::FunctionHandler" +} diff --git a/tests/integration/testdata/buildcmd/Java/gradle-kotlin/build.gradle.kts b/tests/integration/testdata/buildcmd/Java/gradle-kotlin/build.gradle.kts new file mode 100644 index 0000000000..216a5bfec1 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradle-kotlin/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + 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-kotlin/src/main/java/aws/example/Hello.java b/tests/integration/testdata/buildcmd/Java/gradle-kotlin/src/main/java/aws/example/Hello.java new file mode 100644 index 0000000000..db02d37583 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Java/gradle-kotlin/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/unit/commands/buildcmd/test_build_context.py b/tests/unit/commands/buildcmd/test_build_context.py index 193b899bb6..ce9e413cef 100644 --- a/tests/unit/commands/buildcmd/test_build_context.py +++ b/tests/unit/commands/buildcmd/test_build_context.py @@ -1,5 +1,4 @@ import os - from unittest import TestCase from mock import patch, Mock @@ -28,7 +27,8 @@ def test_must_setup_context(self, ContainerManagerMock, pathlib_mock, SamFunctio use_container=True, docker_network="network", parameter_overrides="overrides", - skip_pull_image=True) + skip_pull_image=True, + mode="buildmode") setup_build_dir_mock = Mock() build_dir_result = setup_build_dir_mock.return_value = "my/new/build/dir" context._setup_build_dir = setup_build_dir_mock @@ -45,6 +45,7 @@ def test_must_setup_context(self, ContainerManagerMock, pathlib_mock, SamFunctio self.assertEquals(context.use_container, True) self.assertEquals(context.output_template_path, os.path.join(build_dir_result, "template.yaml")) self.assertEquals(context.manifest_path_override, os.path.abspath("manifest_path")) + self.assertEqual(context.mode, "buildmode") get_template_data_mock.assert_called_once_with("template_file") SamFunctionProviderMock.assert_called_once_with(template_dict, "overrides") @@ -52,3 +53,89 @@ def test_must_setup_context(self, ContainerManagerMock, pathlib_mock, SamFunctio setup_build_dir_mock.assert_called_with("build_dir", True) ContainerManagerMock.assert_called_once_with(docker_network_id="network", skip_pull_image=True) + + +class TestBuildContext_setup_build_dir(TestCase): + + @patch("samcli.commands.build.build_context.shutil") + @patch("samcli.commands.build.build_context.os") + @patch("samcli.commands.build.build_context.pathlib") + def test_build_dir_exists_with_non_empty_dir(self, pathlib_patch, os_patch, shutil_patch): + path_mock = Mock() + pathlib_patch.Path.return_value = path_mock + os_patch.listdir.return_value = True + path_mock.resolve.return_value = "long/full/path" + path_mock.exists.return_value = True + build_dir = "/somepath" + + full_build_path = BuildContext._setup_build_dir(build_dir, True) + + self.assertEquals(full_build_path, "long/full/path") + + os_patch.listdir.assert_called_once() + path_mock.exists.assert_called_once() + path_mock.mkdir.assert_called_once_with(mode=0o755, parents=True, exist_ok=True) + pathlib_patch.Path.assert_called_once_with(build_dir) + shutil_patch.rmtree.assert_called_once_with(build_dir) + + @patch("samcli.commands.build.build_context.shutil") + @patch("samcli.commands.build.build_context.os") + @patch("samcli.commands.build.build_context.pathlib") + def test_build_dir_exists_with_empty_dir(self, pathlib_patch, os_patch, shutil_patch): + path_mock = Mock() + pathlib_patch.Path.return_value = path_mock + os_patch.listdir.return_value = False + path_mock.resolve.return_value = "long/full/path" + path_mock.exists.return_value = True + build_dir = "/somepath" + + full_build_path = BuildContext._setup_build_dir(build_dir, True) + + self.assertEquals(full_build_path, "long/full/path") + + os_patch.listdir.assert_called_once() + path_mock.exists.assert_called_once() + path_mock.mkdir.assert_called_once_with(mode=0o755, parents=True, exist_ok=True) + pathlib_patch.Path.assert_called_once_with(build_dir) + shutil_patch.rmtree.assert_not_called() + + @patch("samcli.commands.build.build_context.shutil") + @patch("samcli.commands.build.build_context.os") + @patch("samcli.commands.build.build_context.pathlib") + def test_build_dir_does_not_exist(self, pathlib_patch, os_patch, shutil_patch): + path_mock = Mock() + pathlib_patch.Path.return_value = path_mock + path_mock.resolve.return_value = "long/full/path" + path_mock.exists.return_value = False + build_dir = "/somepath" + + full_build_path = BuildContext._setup_build_dir(build_dir, True) + + self.assertEquals(full_build_path, "long/full/path") + + os_patch.listdir.assert_not_called() + path_mock.exists.assert_called_once() + path_mock.mkdir.assert_called_once_with(mode=0o755, parents=True, exist_ok=True) + pathlib_patch.Path.assert_called_once_with(build_dir) + shutil_patch.rmtree.assert_not_called() + + @patch("samcli.commands.build.build_context.shutil") + @patch("samcli.commands.build.build_context.os") + @patch("samcli.commands.build.build_context.pathlib") + def test_non_clean_build_when_dir_exists_with_non_empty_dir(self, pathlib_patch, os_patch, shutil_patch): + path_mock = Mock() + pathlib_patch.Path.return_value = path_mock + os_patch.listdir.return_value = True + path_mock.resolve.return_value = "long/full/path" + path_mock.exists.return_value = True + build_dir = "/somepath" + + full_build_path = BuildContext._setup_build_dir(build_dir, False) + + self.assertEquals(full_build_path, "long/full/path") + + os_patch.listdir.assert_called_once() + path_mock.exists.assert_called_once() + path_mock.mkdir.assert_called_once_with(mode=0o755, parents=True, exist_ok=True) + pathlib_patch.Path.assert_called_once_with(build_dir) + shutil_patch.rmtree.assert_not_called() diff --git a/tests/unit/commands/buildcmd/test_command.py b/tests/unit/commands/buildcmd/test_command.py index 00e1b2c112..8b867fe480 100644 --- a/tests/unit/commands/buildcmd/test_command.py +++ b/tests/unit/commands/buildcmd/test_command.py @@ -1,9 +1,11 @@ +import os +import click from unittest import TestCase from mock import Mock, patch from parameterized import parameterized -from samcli.commands.build.command import do_cli +from samcli.commands.build.command import do_cli, _get_mode_value_from_envvar from samcli.commands.exceptions import UserException from samcli.lib.build.app_builder import BuildError, UnsupportedBuilderLibraryVersionError from samcli.lib.build.workflow_config import UnsupportedRuntimeException @@ -29,13 +31,14 @@ def test_must_succeed_build(self, modified_template = builder_mock.update_template.return_value = "modified template" do_cli("template", "base_dir", "build_dir", "clean", "use_container", - "manifest_path", "docker_network", "skip_pull", "parameter_overrides") + "manifest_path", "docker_network", "skip_pull", "parameter_overrides", "mode") ApplicationBuilderMock.assert_called_once_with(ctx_mock.function_provider, ctx_mock.build_dir, ctx_mock.base_dir, manifest_path_override=ctx_mock.manifest_path_override, - container_manager=ctx_mock.container_manager) + container_manager=ctx_mock.container_manager, + mode=ctx_mock.mode) builder_mock.build.assert_called_once() builder_mock.update_template.assert_called_once_with(ctx_mock.template_dict, ctx_mock.original_template_path, @@ -62,6 +65,36 @@ def test_must_catch_known_exceptions(self, exception, ApplicationBuilderMock, Bu with self.assertRaises(UserException) as ctx: do_cli("template", "base_dir", "build_dir", "clean", "use_container", - "manifest_path", "docker_network", "skip_pull", "parameteroverrides") + "manifest_path", "docker_network", "skip_pull", "parameteroverrides", "mode") self.assertEquals(str(ctx.exception), str(exception)) + + +class TestGetModeValueFromEnvvar(TestCase): + + def setUp(self): + self.original = os.environ.copy() + self.varname = "SOME_ENVVAR" + self.choices = ["A", "B", "C"] + + def tearDown(self): + os.environ = self.original + + def test_must_get_value(self): + + os.environ[self.varname] = "A" + result = _get_mode_value_from_envvar(self.varname, self.choices) + + self.assertEquals(result, "A") + + def test_must_raise_if_value_not_in_choice(self): + + os.environ[self.varname] = "Z" + + with self.assertRaises(click.UsageError): + _get_mode_value_from_envvar(self.varname, self.choices) + + def test_return_none_if_value_not_found(self): + + result = _get_mode_value_from_envvar(self.varname, self.choices) + self.assertIsNone(result) diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index 28bdb8298f..a9c628f00d 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -8,7 +8,7 @@ from samcli.lib.build.app_builder import ApplicationBuilder,\ UnsupportedBuilderLibraryVersionError, BuildError, \ - LambdaBuilderError + LambdaBuilderError, ContainerBuildNotSupported class TestApplicationBuilder_build(TestCase): @@ -182,7 +182,8 @@ class TestApplicationBuilder_build_function_in_process(TestCase): def setUp(self): self.builder = ApplicationBuilder(Mock(), "/build/dir", - "/base/dir") + "/base/dir", + mode="mode") @patch("samcli.lib.build.app_builder.LambdaBuilder") def test_must_use_lambda_builder(self, lambda_builder_mock): @@ -194,7 +195,7 @@ def test_must_use_lambda_builder(self, lambda_builder_mock): "artifacts_dir", "scratch_dir", "manifest_path", - "runtime") + "runtime",) self.assertEquals(result, "artifacts_dir") lambda_builder_mock.assert_called_with(language=config_mock.language, @@ -206,7 +207,8 @@ def test_must_use_lambda_builder(self, lambda_builder_mock): "scratch_dir", "manifest_path", runtime="runtime", - executable_search_paths=config_mock.executable_search_paths) + executable_search_paths=config_mock.executable_search_paths, + mode="mode") @patch("samcli.lib.build.app_builder.LambdaBuilder") def test_must_raise_on_error(self, lambda_builder_mock): @@ -231,7 +233,8 @@ def setUp(self): self.builder = ApplicationBuilder(Mock(), "/build/dir", "/base/dir", - container_manager=self.container_manager) + container_manager=self.container_manager, + mode="mode") self.builder._parse_builder_response = Mock() @patch("samcli.lib.build.app_builder.LambdaBuildContainer") @@ -275,7 +278,8 @@ def mock_wait_for_logs(stdout, stderr): log_level=log_level, optimizations=None, options=None, - executable_search_paths=config.executable_search_paths) + executable_search_paths=config.executable_search_paths, + mode="mode") self.container_manager.run.assert_called_with(container_mock) self.builder._parse_builder_response.assert_called_once_with(stdout_data, container_mock.image) @@ -309,6 +313,39 @@ def test_must_raise_on_unsupported_container(self, LambdaBuildContainerMock): self.assertEquals(str(ctx.exception), msg) self.container_manager.stop.assert_called_with(container_mock) + def test_must_raise_on_docker_not_running(self): + config = Mock() + + self.container_manager.is_docker_reachable = False + + with self.assertRaises(BuildError) as ctx: + self.builder._build_function_on_container(config, + "source_dir", + "artifacts_dir", + "scratch_dir", + "manifest_path", + "runtime") + + self.assertEquals(str(ctx.exception), + "Docker is unreachable. Docker needs to be running to build inside a container.") + + @patch("samcli.lib.build.app_builder.supports_build_in_container") + def test_must_raise_on_unsupported_container_build(self, supports_build_in_container_mock): + config = Mock() + + reason = "my reason" + supports_build_in_container_mock.return_value = (False, reason) + + with self.assertRaises(ContainerBuildNotSupported) as ctx: + self.builder._build_function_on_container(config, + "source_dir", + "artifacts_dir", + "scratch_dir", + "manifest_path", + "runtime") + + self.assertEquals(str(ctx.exception), reason) + class TestApplicationBuilder_parse_builder_response(TestCase): diff --git a/tests/unit/lib/build_module/test_workflow_config.py b/tests/unit/lib/build_module/test_workflow_config.py index 86cf41da14..9939d58ff3 100644 --- a/tests/unit/lib/build_module/test_workflow_config.py +++ b/tests/unit/lib/build_module/test_workflow_config.py @@ -51,6 +51,7 @@ def test_must_work_for_ruby(self, runtime): @parameterized.expand([ ("java8", "build.gradle", "gradle"), + ("java8", "build.gradle.kts", "gradle"), ("java8", "pom.xml", "maven") ]) @patch("samcli.lib.build.workflow_config.os") diff --git a/tests/unit/local/docker/test_container.py b/tests/unit/local/docker/test_container.py index 1d7f3b25f7..19ff4033e2 100644 --- a/tests/unit/local/docker/test_container.py +++ b/tests/unit/local/docker/test_container.py @@ -76,7 +76,7 @@ def test_must_create_container_with_required_values(self): expected_volumes = { self.host_dir: { "bind": self.working_dir, - "mode": "ro" + "mode": "ro,delegated" } } generated_id = "fooobar" @@ -109,7 +109,7 @@ def test_must_create_container_including_all_optional_values(self): expected_volumes = { self.host_dir: { "bind": self.working_dir, - "mode": "ro" + "mode": "ro,delegated" }, '/somepath': {"blah": "blah value"} } @@ -167,7 +167,7 @@ def test_must_create_container_translate_volume_path(self, os_mock): translated_volumes = { "/c/Users/Username/AppData/Local/Temp/tmp1337": { "bind": self.working_dir, - "mode": "ro" + "mode": "ro,delegated" } } @@ -222,7 +222,7 @@ def test_must_connect_to_network_on_create(self): expected_volumes = { self.host_dir: { "bind": self.working_dir, - "mode": "ro" + "mode": "ro,delegated" } } @@ -264,7 +264,7 @@ def test_must_connect_to_host_network_on_create(self): expected_volumes = { self.host_dir: { "bind": self.working_dir, - "mode": "ro" + "mode": "ro,delegated" } } diff --git a/tests/unit/local/docker/test_lambda_build_container.py b/tests/unit/local/docker/test_lambda_build_container.py index 8c5421b604..66901962fe 100644 --- a/tests/unit/local/docker/test_lambda_build_container.py +++ b/tests/unit/local/docker/test_lambda_build_container.py @@ -46,7 +46,8 @@ def test_must_init_class(self, "runtime", optimizations="optimizations", options="options", - log_level="log-level") + log_level="log-level", + mode="mode") self.assertEquals(container.image, image) self.assertEquals(container.executable_name, "lambda-builders") @@ -93,7 +94,8 @@ def test_must_make_request_object_string(self): "runtime", "optimizations", "options", - "executable_search_paths") + "executable_search_paths", + "mode") self.maxDiff = None # Print whole json diff self.assertEqual(json.loads(result), { @@ -114,7 +116,8 @@ def test_must_make_request_object_string(self): "runtime": "runtime", "optimizations": "optimizations", "options": "options", - "executable_search_paths": "executable_search_paths" + "executable_search_paths": "executable_search_paths", + "mode": "mode" } })