diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 98aac4ffab..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,50 +0,0 @@ -# Enable container based builds -sudo: required -language: python -dist: xenial - -services: - - docker - -python: - - "2.7" - - "3.6" - - "3.7" - - "3.8-dev" - -matrix: - allow_failures: - - python: 3.8-dev - -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 - # 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 - - make init - -script: - # Runs unit tests - - make pr - - SAM_CLI_DEV=1 travis_wait pytest -vv tests/integration diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md index 40f0455f33..f6e916f426 100644 --- a/DEVELOPMENT_GUIDE.md +++ b/DEVELOPMENT_GUIDE.md @@ -154,6 +154,39 @@ conventions are best practices that we have learnt over time. - Do not catch the broader `Exception`, unless you have a really strong reason to do. You must explain the reason in great detail in comments. + + +Testing +------- + +We need thorough test coverage to ensure the code change works today, +and continues to work in future. When you make a code change, use the +following framework to decide the kinds of tests to write: + +- When you adds/removed/modifies code paths (aka branches/arcs), + write **unit tests** with goal of making sure the flow works. Focus + on verifying the flow and use mocks to isolate from as many + external dependencies as you can. "External dependencies" + includes system calls, libraries, other classes/methods you wrote + but logically outside of the system-under-test. + + > Aim to test with complete isolation + +- When your code uses external dependencies, write **functional tests** + to verify some flows by including as many external dependencies as + possible. Focus on verifying the flows that directly use the dependencies. + + > Aim to test one or more logically related components. Includes docker, + file system, API server, but might still mock some things like AWS API + calls. + +- When your code adds/removes/modifies a customer facing behavior, + write **integration tests**. Focus on verifying the customer experience + works as expected. + + > Aim to test how a customer will use the feature/command. Includes + calling AWS APIs, spinning up Docker containers, mutating files etc. + Design Document --------------- diff --git a/LICENSE b/LICENSE index d645695673..59403b3a24 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/appveyor.yml b/appveyor.yml index 18e6af57e5..0e53fc71ef 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,48 +1,69 @@ version: 1.0.{build} -image: Visual Studio 2017 -build: off +image: + - Ubuntu + - Visual Studio 2017 environment: AWS_DEFAULT_REGION: us-east-1 SAM_CLI_DEV: 1 - + matrix: + - PYTHON_HOME: "C:\\Python27-x64" - PYTHON_VERSION: '2.7' + PYTHON_VERSION: '2.7.16' PYTHON_ARCH: '32' - PYTHON_HOME: "C:\\Python36-x64" - PYTHON_VERSION: '3.6' + PYTHON_VERSION: '3.6.8' PYTHON_ARCH: '64' - # Testing on both 32bit and 64bit Windows only for Latest Python version, - # because MSIs installers use Latest Python version - - PYTHON_HOME: "C:\\Python37" - PYTHON_VERSION: '3.7' - PYTHON_ARCH: '32' - - PYTHON_HOME: "C:\\Python37-x64" - PYTHON_VERSION: '3.7' + PYTHON_VERSION: '3.7.4' PYTHON_ARCH: '64' -install: +for: + - + matrix: + only: + - image: Visual Studio 2017 + + install: + # Upgrade setuptools, wheel and virtualenv + - "python -m pip install --upgrade setuptools wheel virtualenv" + + # Create new virtual environment and activate it + - "rm -rf venv" + - "python -m virtualenv venv" + - "venv/Scripts/activate" + + - + matrix: + only: + - image: Ubuntu + install: + - sh: "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" + - sh: "PATH=$JAVA_HOME/bin:$PATH" + - sh: "source ${HOME}/venv${PYTHON_VERSION}/bin/activate" + - sh: "rvm use 2.5" - # Upgrade setuptools, wheel and virtualenv - - "python -m pip install --upgrade setuptools wheel virtualenv" + # Install latest gradle + - sh: "sudo apt-get -y remove gradle" + - sh: "wget https://services.gradle.org/distributions/gradle-5.5-bin.zip -P /tmp" + - sh: "sudo unzip -d /opt/gradle /tmp/gradle-*.zip" + - sh: "PATH=/opt/gradle/gradle-5.5/bin:$PATH" - # Create new virtual environment and activate it - - "rm -rf venv" - - "python -m virtualenv venv" - - "venv\\Scripts\\activate" - - "python -c \"import sys; print(sys.executable)\"" +build_script: + - "python -c \"import sys; print(sys.executable)\"" - # Actually install SAM CLI's dependencies - - "pip install -e \".[dev]\"" + # Actually install SAM CLI's dependencies + - "pip install -e \".[dev]\"" test_script: - - "pytest --cov samcli --cov-report term-missing --cov-fail-under 95 tests\\unit" + - "pytest --cov samcli --cov-report term-missing --cov-fail-under 95 tests/unit" - "flake8 samcli" - - "flake8 tests\\unit tests\\integration" + - "flake8 tests/unit tests/integration" - "pylint --rcfile .pylintrc samcli" + # Runs only in Linux + - sh: "pytest -vv tests/integration" diff --git a/designs/cloud_formation_api_gateway_support.md b/designs/cloud_formation_api_gateway_support.md new file mode 100644 index 0000000000..fafbe27d6c --- /dev/null +++ b/designs/cloud_formation_api_gateway_support.md @@ -0,0 +1,354 @@ +# CloudFormation ApiGateway Support Design Doc + +## The Problem + +Customers use SAM CLI to run/test their applications by defining their resources in a SAM template. The raw CloudFormation ApiGateway resources are not currently supported to run and test locally in SAM CLI. The resource types include AWS::ApiGateway::* while ```sam local start-api``` only supports the AWS::Serverless::Api type. This prevents customers who have built/deployed services using the raw ApiGateway Resources or who have used tools to generate CloudFormation, like AWS CDK, from testing locally through SAM CLI. Specifically, Customers that generate their CloudFormation template using tools and have hand written CloudFormation templates have AWS::ApiGateway::* types preventing them from running locally. + +Customers that are able to test and run locally can find errors early, reduce development time, etc. If customers are not able to test, the development cycles will be very long with a process similar to write code, deploy, deploy failed, investigate, fix and repeat. + +## Who are the Customers? + +* People who work with tools such as AWS CDK, Terraform, and others to generate their CloudFormation templates +* People that have hand written CloudFormation templates with ApiGateway Resources + +## Success criteria for the change + +* Customers would be able to author SAM templates with Api Gateway Resources and test locally through start-api +* Feature parity with what is currently supported with start-api on the AWS::Serverless::Api Resource +* SAM CLI should support swagger and vanilla CloudFormation Api Gateway resources + +Overall, SAM CLI should be able to seamlessly support local development and testing with the Api Gateway CloudFormation Resources. + +## What will be changed? + +When customers run ```sam local start-api``` with a template that uses raw CloudFormation AWS::ApiGateway::RestApi and AWS::ApiGateway::Stage resources, they will be able to interact and test their lambda functions as if they were using AWS::Serverless::Api. + +## Out-of-Scope + +Anything that SAM CLI doesn't currently support in SAM +* ApiGateway Authorization such as resource policies, IAM roles/tags/policies, Lambda Authorizers, etc. +* ApiGateway CORS support +* Proper validation of the CloudFormation templates so that it does smart validation and not just yaml parsing. + +## User Experience Walkthrough + +There are two main types of users who are going to benefit from this change. + +* Customers can use tools such as AWS CDK to generate a template. The customer can create their AWS CDK project with `cdk init app` and then generate their CloudFormation code using `cdk synth.`They can input their CloudFormation code to test it locally using the SAM CLI command. +* Customers can author CloudFormation resources and test them locally by inputting their templates into sam local start-api. + +For both cases, The code can be run locally if they have CodeUri's pointing to valid local paths. + +Once the user has their CloudFormation code, they will be running `sam local start-api --template /path/to/template.yaml` + +# Implementation + +## **Design** + +There are a few approaches to supporting the new CloudFormation ApiGateway types such as ApiGateway::RestApi. + +### *Approach #1*: Parsing both CloudFormation and SAM Resources + +Appending to the current Sam Api code and to have dual support of both the CloudFormation Template and SAM template + +Pros: +* This approach is something we do for lambda functions such as AWS::Lambda::Function and will provide consistency with our current implementation. + +Cons: +* Managing two different systems/templates require more work to resolve bugs. For example, supporting ApiGatewayV2, which supports web sockets with ApiGateway, the parsing of the template will need to be reimplemented in two places in order to support new functionality. One where it was defined using CloudFormation and the other where it was defined using the SAM template. This will slowly start to incur more and more technical debt as there is now more area to cover and duplication of work and resources. In the short term, it may be easier to implement, but in the long term there may be issues when dealing with support. +* Another issue is that there may be escape hatches that are implemented in SAM causing additional effort to maintain differing parsing parts of the codebase. + +### *Approach #2*: Process Once + +Convert the SAM template into CloudFormation code and processes it once. + +Pros: +* This simplifies a lot of the issues with approach #1. This will make it much easier to add and extend the system such as adding ApiGatewayV2, web sockets. The feature would only need to be added once instead of duplicated effort. + +Cons: +* This will require more work in order to restructure the application such that the template is processed once after it processed to CloudFormation. +* This could also produce bugs with issues if the SAM to CloudFormation transformation isn't correct in local testing. However, this may not matter as much since there is a direct translation of SAM resources to CloudFormation Resources and only a single point where the bug needs to be fixed. +* Possible imperfections of the SAM to CloudFormation conversion. In the past, some version of the transformation caused incompatible changes with the SAM CLI. If the conversion fails, it could cause users that are currently running their code locally to break. +* The SAM Translator also requires credentials, which would now cause users to login before running a command. Credentials are currently required in sam validate because of this. A local flavor of the translator needs to be created in order to avoid the process. + +### *Approach #3*: Abstraction + +Extending the current code to a CloudFormationApiProvider object overriding the CloudFormation processing in certain methods. This will be doing two passes through the code. SAM code can be processed first and the CloudFormation types second. + +Pros: +* This provides better abstraction, separating the CloudFormation code and the SamApi code. This can also be easily implemented. + +Cons: +* This falls through the same pitfalls as approach #1. The abstraction for the same types may also be inconsistent with the way we are currently processing CloudFormation resources such as AWS::Lambda::Function. + +### Approach RECOMMENDATION + +Although other parts of the project are using approach #1 currently, I recommend using **approach #2**. It seems to cause the least technical debt in the long run and reduces our duplication of code. It may require some work to port some of the code from AWS::Lambda::Function to follow a similar format, but it may be worth in the long term. Since the act of translating the SAM to CloudFormation may be imperfect. I plan to first implement approach #1 and gradually move it to approach #2 once all the items are parsed. + +## **Design: Api Details** + +### **AWS::ApiGateway::RestApi** + +AWS::ApiGateway::RestApi will be one of the main resources defined in the CloudFormation template. It acts as the main definition for the Api. There are two approaches to consider for defining the RestApi template. + +***Feature #1*: Swagger Method** +The swagger method is very similar to the support in our current code base. Swagger has many advantageous as it can be exported, do validation, etc. Customers can also link to the other files containing their swagger documentation. + +```yaml +ServerlessRestApi: +Type: 'AWS::ApiGateway::RestApi' +Properties: + Body: + basePath : /pete + paths: + /hello: + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: !Sub >- + arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations + responses: {} +``` + +Swagger can also be inlined using the FN::Include Macro. This should also be supported while defining the CloudFormation template. +****Feature* #2*: Using a combination of AWS::ApiGateway::Resource with ApiGateway::Methods.** + +Although this approach is less common and more verbose, it is still used by some people while defining their resources. Tools +such as aws-cdk currently generate Resource and Methods ApiGateway CloudFormation Resources in their yaml instead of swagger. + +```yaml +UsersResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId:!Ref RestApi + ParentId: + Fn::GetAtt: + - RestApi + - RootResourceId + PathPart: user +UsersGet: + Type: AWS::ApiGateway::Method + Properties: + ResourceId: !Ref ApiGatewayResource + RestApiId: !Ref ApiGatewayRestApi + Integration: + Type: AWS + IntegrationHttpMethod: POST + Uri: + Fn::Join: + - '' + - - 'arn:aws:apigateway:' + - Ref: AWS::Region + - ":lambda:path/2015-03-31/functions/" + - Fn::GetAtt: + - Lambda + - Arn + - "/invocations" + IntegrationResponses: [] + MethodResponses: + - ResponseModels: + application/json: + Ref: UsersModel + ResponseParameters: + method.response.header.Link: true + StatusCode: 200 + ApiGatewayMethod: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: POST + Integration: + ConnectionType: INTERNET + IntegrationResponses: + - ResponseTemplates: + application/json: "{\"message\": \"OK\"}" + StatusCode: 200 + RequestTemplates: + applicationjson: "{\"statusCode\": $input.json('$.statusCode'), \"message\": $input.json('$.message')}" + +``` + +The SAM template requires people to use only a single RestApi to define all of their resources. + +### **AWS::ApiGateway::Stages** + +The stage allows for defining environments for different parts of the pipeline in CloudFormation. Since a single stage is attached to a deployment, the behavior of multiple deployment/stages locally is currently undefined. + +Currently, the SAM template supports only one stage/one deployment and does not // as customers could want. One approach is to support additional flags in environments and stage names so they can test the apis locally in multiple environments. This would create a dict such as {“dev”: [models], “prod”: [models]}. However, this may be unnecessary. + +The AWS::ApiGateway::Stage will initially support the schema: + +```yaml +Prod: +Type: AWS::ApiGateway::Stage +Properties: + StageName: Prod + Description: Prod Stage + Variables: + Stack: Prod + RestApiId: RestApi` +``` + +Since the stages can show up in any order as the code is iterating through the resources, There are two approaches to consider too quickly stitch the api and stages together. + +*Approach #1:* +Pick the last stage in the template that we find and use that on all the Apis +Pros: Similar to the current approach +Cons: The customer may not be able to view their environments in different stages. + +*Approach: #2: * +Create a dictionary of all the stages and display the url as localhost:3000//routeName +Pros: This will allow people to view their code in different stages or environments. +Cons: This will require refactoring parts of the SamApiProvider that handles the Api. Customers usually don't use different stages to test and it may not be worth the effort to test. + +I recommend approach #1 as it follows the current standard and there is some data that very little people use the when defining it. + +**AWS::ApiGateway::Deployment ** +The deployment type defines which Apis objects should be available to the public. Normally, in the SAM template everything defined will have a deployment component, but the CloudFormation templates may have more bloat and information in them defining certain apis and deployments for Multi-stages. + +One approach is to support multiple stages with multiple deployments and the code will filter out the stages that are not deployed or defined. One problem with this approach is that the RestApiId, a characteristic of the RestApi, for a deployment is only localhost and a single endpoint and the current schema doesn't allow deploying on multiple ports and multiple local domains. This will bring in unneeded complexity such as requiring the customer defining N valid ports and N valid domains. + +Another approach, which I recommend, is to ignore the resource altogether as it always associated with a stage and to randomly pick a stage. + +## **Updating the code** + +First, the Apis in AWS::ApiGateway::RestApi will be parsed in the SAM Template in a similar way to the current AWS::Serverless:Api code. Stages and other Meta information in the Api will be parsed to support the CloudFormation template. The code will then be refactored to first translate the SAM templates inputted into the CloudFormation template using the SamTranslator. Once the Api code has been refactored, the function code can be refactored so that Serverless code is refactored out. + +Some pseudo classes and function for the implementation + +```python + +class CloudFormationGatewayProvider(SamBaseProvider): + # Same as serverless but update with ApiGateway Code + _GATEWAY_REST_API = "AWS::ApiGateway::RestApi" + _GATEWAY_STAGE_API = "AWS::ApiGateway::Stages" + _GATEWAY_RESOURCE_API = "AWS::ApiGateway::Resource" + _GATEWAY_METHODS_API = "AWS::ApiGateway::Method" + + def _extrac_api_gateway_rest_api(): + for logical_id, resource in resources.items(): + resource_type = resource.get(SamApiProvider._TYPE) + # call method based on matching type + + # Detect Swagger or Method/Resource approach + # Parse code similar to previous approach and update api dict + # If the data is not swagger add the unfinished Api + def _extract_gateway_rest_api(): + pass + + # set the method_name for the Api from AWS::ApiGateway::Methods + def _extract_gateway_methods(): + pass + + # Set the stage for that api and its resources using AWS::ApiGateway::Stage + def _extract_gateway_stage(): + pass + + # The alterate method for defining resources. This will parse the Api and update the path + def _extract_gateway_resource(): + pass + + # Update the Api Model with the method name that corressponds to it. + def _extract_gateway_method(): + pass +``` + +While updating the structure and flow of the code around Apis, parts of the flow about how the Api and Route is currently implemented can be updated to make updating future milestones like cors. Some examples include abstracting stage states and variables that exist in every route can be abstracted. + +## CLI Changes + +*Explain the changes to command line interface, including adding new commands, modifying arguments etc* +None + +### Breaking Change + +*Are there any breaking changes to CLI interface? Explain* + +None + +## `.samrc` Changes + +*Explain the new configuration entries, if any, you want to add to .samrc* + +None + +## 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?** +None + +**What other Docker container images are you using?** +None + +**Are you creating a new HTTP endpoint? If so explain how it will be created & used** +This will be used for local development. + +**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?** +The setup is not written for local development. + +# What is your Testing Plan (QA)? + +## Goal + +## Pre-Requisites + +## Test Scenarios/Cases + +* Basic Templates and unit tests to check that CloudFormation templates are covering the data +* Go through https://github.com/awslabs/serverless-application-model with tests/sample inputs and outputs. +* Create an example AWS Lambda Project with AWS CDK and test the generated CloudFormation code + +## Expected Results + +## Pass/Fail + +# Documentation Changes + +The main documentation change will be telling users that they are now allowed to pass SAM templates with ApiGateway resources. + +# Open Issues + +No open issues + +# Task Breakdown + +### Milestones: + +**Milestone 1 Goal: Support Swagger and Stage Name with CloudFormation ApiGateway Resources** +Milestone #1A + +* Swagger Definition for AWS::ApiGateway works for RestApi + +Milestone #1B: + +* Support Stage Names/Variables, binary_media_types with AWS::ApiGatway + +Milestone #1C: + +* Support Non-inline swagger + +Milestone #1D: + +* Refactor Code to convert SAM into CloudFormationResource + +**Milestone #2: Support Resource and Methods with CloudFormation ApiGateway Resources** + +* Add Support for Resource and Methods parsing + +### Time Breakdown + +Milestone 1A ~ 1 Week + +Milestone 1B ~ 1 Week + +Milestone 1C ~ 2 day + +Milestone 1D ~ 1 Week + +Milestone 2 ~ 1 Week + + diff --git a/designs/intrinsics_design.md b/designs/intrinsics_design.md new file mode 100644 index 0000000000..b0848238d0 --- /dev/null +++ b/designs/intrinsics_design.md @@ -0,0 +1,410 @@ +# Intrinsic Function Support Design Doc + +## The Problem + +Customers can define their CloudFormation resources in many ways. Intrinsic Functions allow for greater templating and modularity of the CloudFormation template by injecting properties at runtime. Intrinsic Functions have the properties `Fn::Base64, Fn::FindInMap, Ref, Fn::Join, etc.` This is also true in the SAM template, which supports a small parts of the Intrinsic functions. Although customers use a variety of attributes like Fn::Join, Ref regularly, SAM-CLI is unable to run and resolve it locally. This prevents customers from testing and running their code locally, leading in frustration and problems. + +Intrinsic Functions are also used in many of the tools that generate CloudFormation. For example, AWS-CDK generates their CloudFormation resources using `Fn::Join, Fn::GetAtt, Ref` with AWS::ApiGateway::Resource and AWS::ApiGateway::Methods, which fails to resolve and run locally. Supporting this will allow for greater interoperability with other tools, creating a better local developer experience. + +In terms of resolving intrinsic properties, there are no other tools for local space. This makes it difficult for other tools to build any code that has intrinsic functions. + +## Who are the Customers? + +* Customers who work with tools such as Serverless, AWS CDK, Terraform, and others to generate their CloudFormation templates due to the increased interoperability with SAM-CLI +* Customers who create tools such as Serverless, AWS CDK, etc. to resolve Intrinsic properties to create their systems, improving the developer tooling space. +* Customers who work create SAM/CloudFormation templates that involve intrinsics + +## Success criteria for the change + +* The intrinsic properties Fn::Base64, Fn::And, Fn::Equals, Fn::If ,Fn::Not. Fn::Or, Fn::GetAtt, Fn::GetAZs, Fn::Join, Fn::Select, Fn::Split, Fn::Sub, Ref will all work locally, mirroring the attributes CloudFormation does. +* AWS-CDK and Serverless generate templates that can be processed and run locally directly within SAM-CLI + +## What will be changed? + +The code will now recursively parse the different intrinsic function properties and resolve them at runtime. + +## Out-of-Scope + +The following intrinsics are out of scope: + +* Macros with Fn::Transform other than AWS::Include +* Fn::ImportValue and dealing with nested stacks +* Fn::Cidr +* The service based intrinsics https://docs.aws.amazon.com/servicecatalog/latest/adminguide/intrinsic-function-reference-rules.html + +## User Experience Walkthrough +The customer can input a CloudFormation template into SAM-CLI containing intrinsic functions like `Fn::Join, Fn::GetAtt, Ref`. This includes sam build, sam local start-api, etc. + +Example walkthrough with `sam local start-api`: +* Customers can use tools such as AWS CDK to generate a template. The templates will have intrinsic properties. The customer can create their AWS CDK project with `cdk init app` and then generate their CloudFormation code using `cdk synth.`They can input their CloudFormation code to test it locally using the SAM CLI command. +* Customers can author CloudFormation resources with intrinsics functions and test them locally by inputting their templates into sam local start-api. + +Once the user has their CloudFormation code, they will be running `sam local start-api --template /path/to/template.yaml` + +# Implementation + +## Intrinsic Function Properties + +### Fn::Join + +``` +{ "Fn::Join" : [ "delimiter", [ comma-delimited list of values ] ] } +!Join [ "delimiter", [comma-delimited list of values ] ]`` +``` + +This intrinsic function will first verify the objects are a list. +Then for every item in the list, it will recursively run the intrinsic function resolver. +After verifying the types, It will join the items in the list together based on the string. + +### Fn::Split + +``` +{ "Fn::Split" : [ "delimiter", "source string" ] } +!Split `[ "delimiter", "source string" ]`` +``` + +This intrinsic function will recursively resolve every item in the list and then split the source string based on the delimiter. + +### Fn::Base64 + +``` +{ "Fn::Base64" : `valueToEncode` } +!Base64 valueToEncode +``` + +This intrinsic function will resolve the valueToEncode property and then run a python base64 encode on the returned string. + +### Fn::Select + +``` +{ "Fn::Select" : [ index, listOfObjects ] } +!Select [ index, listOfObjects]`` +``` + +This intrinsic function will recursively resolve every item in the list and verify the type of the index element. Then it will select a single item from the listOfObjects that were resolved. + +### Fn:::GetAzs + +``` +Fn::GetAZs: !Ref 'AWS::Region'` +``` + +This intrinsic function will find the region from the reference property and return a list of availability zones. This will be a lookup in this dictionary + +``` +regions = {"us-east-1": ["us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d", "us-east-1e", "us-east-1f"], "us-west-1": ["us-west-1b", "us-west-1c"], "eu-north-1": ["eu-north-1a", "eu-north-1b", "eu-north-1c"], "ap-northeast-3": ["ap-northeast-3a"], "ap-northeast-2": ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"], "ap-northeast-1": ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"], "sa-east-1": ["sa-east-1a", "sa-east-1c"], "ap-southeast-1": ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"], "ca-central-1": ["ca-central-1a", "ca-central-1b"], "ap-southeast-2": ["ap-southeast-2a", "ap-southeast-2b", "ap-southeast-2c"], "us-west-2": ["us-west-2a", "us-west-2b", "us-west-2c", "us-west-2d"], "us-east-2": ["us-east-2a", "us-east-2b", "us-east-2c"], "ap-south-1": ["ap-south-1a", "ap-south-1b", "ap-south-1c"], "eu-central-1": ["eu-central-1a", "eu-central-1b", "eu-central-1c"], "eu-west-1": ["eu-west-1a", "eu-west-1b", "eu-west-1c"], "eu-west-2": ["eu-west-2a", "eu-west-2b", "eu-west-2c"], "eu-west-3": ["eu-west-3a", "eu-west-3b", "eu-west-3c"], "cn-north-1": []} +``` + +### Fn::GetAtt + +``` +"Fn::GetAtt": ["logical_id", "resource_property_type"] +!GetAtt ["logical_id", "resource_property_type"] +``` + +This intrinsic function is one of the harder properties to resolve as it can be injected at runtime. +There is a list of supported properties at https://d1uauaxba7bl26.cloudfront.net/latest/gzip/CloudFormationResourceSpecification.json. +These properties will be read and verified if existed in the ResourceSpecification. In strings, they are usually represented as ${MyInstance.PublicIp}. +Different Types have different parameters attached with them. + +After parsing the logical_id and resource_property_type, a separate symbol table can be used to translate logical_id and resource_id into the injected runtime property. +The symbol resolver will be passed in at runtime separate from the intrinsic_resolver. + + +### `Fn::Sub` + +``` +{ "Fn::Sub" : String } or { "Fn::Sub" : [string, "{test}", "{test2}"] } +``` + +This intrinsic does string substitution. Both types are supported. For the string, Regex will be used to replace the property of the string with the variables that are refs or pseudo refs such as ${AWS::RegionName}. This same process is done with the list, but every item in the list is recursively resolved and then approached the same way. + +### Fn::Transform + +``` +Fn::Transform +``` + +This allows for transforming properties of properties from one format to another. This can allow for many different macro types. However, only *AWS::INCLUDE* is in scope for this project. + +``` +{ + "Fn::Transform": { + "Name": "AWS::Include", + "Parameters": { + "Location": "s3://MyAmazonS3BucketName/swagger.yaml" + } + } +} +``` + +We currently parse this format to figure out the location of the swagger. However, the location paramater can also be resolved as a ref. All that’s left is to run the recursive resolver. + +### Ref + +``` +Ref: logicalName +or +!Ref logicalName +``` + +This intrinsics allows for getting the reference to another resource or parameter. Locally, if it’s a pseudo parameter we should be able to generate a random resolution or come up with the best possible alternative. +These can also be resolved at runtime, so it’s best to make them dynamic functions that are passed in. Unresolved items can be returned as a string of the format ${}. + +Different Types have different parameters attached with them. This feels like it needs to be hardcoded for each one. This can be built dynamically + +### Fn::FindInMap + +``` +{ "Fn::FindInMap" : [ "MapName", "TopLevelKey", "SecondLevelKey"] } +``` + +This resource allows for finding keys and values in a Mappings dictionary. This requires recurcively resolving each property and then getting the property. + +## Boolean Intrinsic Logic + +Customers can also specify boolean logic when trying to resolve the templates. These include Fn::And, Fn::Equals,Fn::If, and Fn::Not. This requires parsing the parameters section and the Conditionals section in the CloudFormation template. When called, the condition needs to be resolved based on the parameters. + +### FN::And + +``` +"Fn::And": [{condition}, {...}] +``` + +This will recursively resolve every property in the list in Fn::And and verify each one returns a boolean true value. The items in the list support both other Intrinsics like Fn::Equals or conditionals of the format {Conditional: ConditionName}. + +### FN::Equals + +``` +"Fn::Equals" : ["value_1", "value_2"] +``` + +This will check that both items in the list will recursively resolve to the same thing. This will return a boolean value. + +### FN::If + +``` +"Fn::If": [condition_name, value_if_true, value_if_false] +``` + +This intrinsic boolean property will first resolve every item in the list, resolving the Conditions part of the template and then selecting value_if_true or value_if_false depending on the attribute. + +### FN::Not + +``` +"Fn::Not": [{condition}] +``` + +This intrinsic function will resolve the condition in the list and then return the opposite boolean value returned. + +### FN::Or + +``` +"Fn::Or": [{condition}, {...}] +``` + +This intrinsic function is very similar to the Fn::And function, but will check that at least one of the items in the last returns a truthy value. + +## Pseudo Parameters + +Pseudo Parameters are predefined by AWS CloudFormation such as the AccountId and such. These will be resolved separately and is heavily used with Refs!. +If the item is specified as an environment setting optionally or it is specified in the Parameter section it will be read there. +Otherwise, These will be resolved whenever a !Ref Psuedo Paramater or Ref: Psuedo Paramater or ${Psuedo Paramater} is in the template. + +The default values for parameters will be +```python + _DEFAULT_PSEUDO_PARAM_VALUES = { + "AWS::AccountId": "123456789012", + "AWS::Partition": "aws", + + "AWS::Region": "us-east-1", + + "AWS::StackName": "local", + "AWS::StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/" + "local/51af3dc0-da77-11e4-872e-1234567db123", + "AWS::URLSuffix": "localhost" + } +``` +### AWS::AccountId + +This will be a account id from the __DEFAULT_PSEUDO_PARAM_VALUES. + +### AWs::NotificationArns + +This property can be randomly generated to follow the format + +### AWS::NoValue + +This will return the None python value. + +### AWS::Partition + +This is resource specific and separates regions like US and China into subgroups. This will be found in a large dictionary. + +### AWS::REGION + +This property will first be attempted to be read from the environment settings. Otherwise, It will use the default region. + +### AWS::StackId + +This will be a stack id from the __DEFAULT_PSEUDO_PARAM_VALUES. + + +### AWS::StackName + +This will be a stack name from the __DEFAULT_PSEUDO_PARAM_VALUES. + +### AWS::URLSuffix + +This will be replaces by the corresponding [amazonaws.com](http://amazonaws.com/) or [amazonaws.com.cn](http://amazonaws.com.cn/) depending on the region settings. This will be found in a dictionary. + +## Implementing the Code + +This can be separated into a IntrinsicsResolver and a SymbolResolver. The SymbolResolver is an abstraction that holds all the translation of the references and attributes inside the template. +The IntrinsicsResolver will recursively parse the template and all the attributes and the symbol table will be injected in at runtime. + +### Intrinsic Symbol Table + +* logical_id_translator: This will be used as an exact translation for translating pseudo/logical_id types + +* default_type_resolver: This will be in the following form, resolving the type + +``` +{ + "AWS::ApiGateway::RestApi": { + "RootResourceId": "/" + } +} +``` + +* common_attribute_resolver: resolves common attributes that will be true across all + +``` +{ "Ref": lambda p,r: "", + "Arn:": arn_resolver} +``` + + +First pseudo types are checked. If item is present in the logical_id_translator it is returned. Otherwise, it falls back to the default_pseudo_resolver. + +Then the default_type_resolver is checked, which has common attributes and functions for each types. + +Then the common_attribute_resolver is run, which has functions that are common for each attribute. + +### Intrinsic Resolver + +The will be the core part of the resolver, which will be recursively called. + +``` +def intrinsic_property_resolver(): + if key in self.intrinsic_key_function_map: + # process intrinsic function + elif key in self.conditional_key_function_map: + # process conditional + else: + # In this case, it is a dictionary that doesn't directly contain an intrinsic resolver and must be + # re-parsed to resolve. +``` + +The template will be resolved item by item + +``` +processed_template = {} +for key, val in self.resources.items(): + processed_key = self.symbol_resolver.get_translation(key, IntrinsicResolver.REF) or key + + processed_resource = self.intrinsic_property_resolver(val) + processed_template[processed_key] = processed_resource +``` +If there is an error with the resource, the item will be ignored and processed. This is because we don't want to break any workflows that have errors with unresolved symbol tables. This is especially true for refs that exist to cloud instances. The error will be printed in the console tho. +Currently, in SAM these properties are ignored and would not cause any errors, so by ignoring the errors we are using the same functionality. To ignore the error, we just copy the resource as is. + +## Integration into SAM-CLI + +This can be very easily plugged into the current SAM-CLI code. This needs to be run after the SamBaseTranslator runs. This will go through every property and check if it requires intrinsic resolution. + +This can also be plugged in into specific areas such as the Body section of a Api and the Uri section in a function to handle the Arn. + +## CLI Changes + +*Explain the changes to command line interface, including adding new commands, modifying arguments etc* +None + +### Breaking Change + +*Are there any breaking changes to CLI interface? Explain* + +None + +## `.samrc` Changes + +*Explain the new configuration entries, if any, you want to add to .samrc* + +None + +## 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?** +None + +**What other Docker container images are you using?** +None + +**Are you creating a new HTTP endpoint? If so explain how it will be created & used** +This will be used for local development. + +**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?** +The setup is not written for local development. + +# What is your Testing Plan (QA)? + +## Validation + +The validation logic is baked into the code, by verifying at each step that it has the right type and the items in it. + +## Goal + +Test the main combinations of intrinsic functions in order to verify that the functions are lazily resolving correctly. + +## Test Scenarios/Cases + +# Documentation Changes + +None + +# Open Issues + +https://github.com/awslabs/aws-sam-cli/issues/1079 +https://github.com/awslabs/aws-sam-cli/issues/1038 +https://github.com/awslabs/aws-sam-cli/issues/826 +https://github.com/awslabs/aws-sam-cli/issues/476 +https://github.com/awslabs/aws-sam-cli/issues/194 + + +# Task Breakdown + +### Milestones: + +**Milestone 1 Goal: Support The Basic Intrinsic Properties other than Fn::GetAtt, Fn::ImportValue, and Ref** + +**Milestone 2 Goal: Support Boolean Properties such as Fn::If, Fn::And, Fn::Or, etc.** + +**Milestone 3 Goal: Support Refs and GetAtt with runtime plugins** + +**Milestone 4 Goals: Organize code so that it can be pluggable by multiple libraries easily** + +### Time Breakdown + +Milestone 1 ~ 1 Week + +Milestone 2 ~ 1 Week + +Milestone 3 ~ 1 Week + +Milestone 4 ~ 1 Week + + diff --git a/requirements/base.txt b/requirements/base.txt index b6ea5d6ee5..fff8a7e1c5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,16 +1,16 @@ six~=1.11.0 chevron~=0.12 -click~=6.7 +click~=7.0 enum34~=1.1.6; python_version<"3.4" Flask~=1.0.2 boto3~=1.9, >=1.9.56 -PyYAML~=3.12 +PyYAML~=5.1 cookiecutter~=1.6.0 -aws-sam-translator==1.10.0 -docker~=3.7.0 +aws-sam-translator==1.13.1 +docker~=4.0 dateparser~=0.7 python-dateutil~=2.6 pathlib2~=2.3.2; python_version<"3.4" requests==2.22.0 -serverlessrepo==0.1.8 +serverlessrepo==0.1.9 aws_lambda_builders==0.3.0 diff --git a/samcli/__init__.py b/samcli/__init__.py index 03c7b75894..79695aa390 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = '0.19.0' +__version__ = '0.20.0' diff --git a/samcli/cli/command.py b/samcli/cli/command.py index 84fd9c0aad..aedf5d3f3c 100644 --- a/samcli/cli/command.py +++ b/samcli/cli/command.py @@ -4,6 +4,7 @@ import logging import importlib +import sys import click logger = logging.getLogger(__name__) @@ -20,6 +21,13 @@ "samcli.commands.publish" } +DEPRECATION_NOTICE = ( + "Warning : AWS SAM CLI will no longer support " + "installations on Python 2.7 starting on October 1st, 2019." + " Install AWS SAM CLI via https://docs.aws.amazon.com/serverless-application-model/" + "latest/developerguide/serverless-sam-cli-install.html for continued support with new versions. \n" +) + class BaseCommand(click.MultiCommand): """ @@ -58,6 +66,9 @@ def __init__(self, cmd_packages=None, *args, **kwargs): self._commands = {} self._commands = BaseCommand._set_commands(cmd_packages) + if sys.version_info.major == 2: + click.secho(DEPRECATION_NOTICE, fg="yellow", err=True) + @staticmethod def _set_commands(package_names): """ diff --git a/samcli/cli/context.py b/samcli/cli/context.py index 2a801bf774..39e641e57c 100644 --- a/samcli/cli/context.py +++ b/samcli/cli/context.py @@ -45,7 +45,8 @@ def debug(self, value): if self._debug: # Turn on debug logging - logging.getLogger().setLevel(logging.DEBUG) + logging.getLogger('samcli').setLevel(logging.DEBUG) + logging.getLogger('aws_lambda_builders').setLevel(logging.DEBUG) @property def region(self): diff --git a/samcli/cli/global_config.py b/samcli/cli/global_config.py index ef563f3c8a..d249ad4350 100644 --- a/samcli/cli/global_config.py +++ b/samcli/cli/global_config.py @@ -47,7 +47,6 @@ def config_dir(self): # Internal Environment variable to customize SAM CLI App Dir. Currently used only by integ tests. app_dir = os.getenv("__SAM_CLI_APP_DIR") self._config_dir = Path(app_dir) if app_dir else Path(click.get_app_dir('AWS SAM', force_posix=True)) - return Path(self._config_dir) @property @@ -76,7 +75,7 @@ def installation_id(self): try: self._installation_id = self._get_or_set_uuid(INSTALLATION_ID_KEY) return self._installation_id - except (ValueError, IOError): + except (ValueError, IOError, OSError): return None @property @@ -112,7 +111,7 @@ def telemetry_enabled(self): try: self._telemetry_enabled = self._get_value(TELEMETRY_ENABLED_KEY) return self._telemetry_enabled - except (ValueError, IOError) as ex: + except (ValueError, IOError, OSError) as ex: LOG.debug("Error when retrieving telemetry_enabled flag", exc_info=ex) return False @@ -164,6 +163,10 @@ def _set_value(self, key, value): return self._set_json_cfg(cfg_path, key, value, json_body) def _create_dir(self): + """ + Creates configuration directory if it does not already exist, otherwise does nothing. + May raise an OSError if we do not have permissions to create the directory. + """ self.config_dir.mkdir(mode=0o700, parents=True, exist_ok=True) def _get_config_file_path(self, filename): diff --git a/samcli/cli/main.py b/samcli/cli/main.py index b480297e04..f2ab6bd46f 100644 --- a/samcli/cli/main.py +++ b/samcli/cli/main.py @@ -8,6 +8,7 @@ from samcli import __version__ from samcli.lib.telemetry.metrics import send_installed_metric +from samcli.lib.utils.sam_logging import SamCliLogger from .options import debug_option, region_option, profile_option from .context import Context from .command import BaseCommand @@ -79,7 +80,6 @@ def cli(ctx): You can find more in-depth guide about the SAM specification here: https://github.com/awslabs/serverless-application-model. """ - if global_cfg.telemetry_enabled is None: enabled = True @@ -95,3 +95,10 @@ def cli(ctx): except (IOError, ValueError) as ex: LOG.debug("Unable to write telemetry flag", exc_info=ex) + + sam_cli_logger = logging.getLogger('samcli') + sam_cli_formatter = logging.Formatter('%(message)s') + lambda_builders_logger = logging.getLogger('aws_lambda_builders') + + SamCliLogger.configure_logger(sam_cli_logger, sam_cli_formatter, logging.INFO) + SamCliLogger.configure_logger(lambda_builders_logger, sam_cli_formatter, logging.INFO) diff --git a/samcli/commands/init/__init__.py b/samcli/commands/init/__init__.py index 7186978c0f..a5915cb24e 100644 --- a/samcli/commands/init/__init__.py +++ b/samcli/commands/init/__init__.py @@ -8,7 +8,7 @@ from samcli.cli.main import pass_context, common_options from samcli.commands.exceptions import UserException -from samcli.local.common.runtime_template import INIT_RUNTIMES, SUPPORTED_DEP_MANAGERS +from samcli.local.common.runtime_template import INIT_RUNTIMES, SUPPORTED_DEP_MANAGERS, DEFAULT_RUNTIME from samcli.local.init import generate_project from samcli.local.init.exceptions import GenerateProjectFailedError from samcli.lib.telemetry.metrics import track_command @@ -19,7 +19,7 @@ @click.command(context_settings=dict(help_option_names=[u'-h', u'--help'])) @click.option('-l', '--location', help="Template location (git, mercurial, http(s), zip, path)") -@click.option('-r', '--runtime', type=click.Choice(INIT_RUNTIMES), default="nodejs8.10", +@click.option('-r', '--runtime', type=click.Choice(INIT_RUNTIMES), default=DEFAULT_RUNTIME, help="Lambda Runtime of your app") @click.option('-d', '--dependency-manager', type=click.Choice(SUPPORTED_DEP_MANAGERS), default=None, help="Dependency manager of your Lambda runtime", required=False) diff --git a/samcli/commands/local/lib/api_collector.py b/samcli/commands/local/lib/api_collector.py new file mode 100644 index 0000000000..4c4c1abe8c --- /dev/null +++ b/samcli/commands/local/lib/api_collector.py @@ -0,0 +1,210 @@ +""" +Class to store the API configurations in the SAM Template. This class helps store both implicit and explicit +routes in a standardized format +""" + +import logging +from collections import defaultdict + +from six import string_types + +from samcli.local.apigw.local_apigw_service import Route +from samcli.commands.local.lib.provider import Api + +LOG = logging.getLogger(__name__) + + +class ApiCollector(object): + + def __init__(self): + # Route properties stored per resource. + self._route_per_resource = defaultdict(list) + + # processed values to be set before creating the api + self._routes = [] + self.binary_media_types_set = set() + self.stage_name = None + self.stage_variables = None + self.cors = None + + def __iter__(self): + """ + Iterator to iterate through all the routes stored in the collector. In each iteration, this yields the + LogicalId of the route resource and a list of routes available in this resource. + Yields + ------- + str + LogicalID of the AWS::Serverless::Api or AWS::ApiGateway::RestApi resource + list samcli.commands.local.lib.provider.Api + List of the API available in this resource along with additional configuration like binary media types. + """ + + for logical_id, _ in self._route_per_resource.items(): + yield logical_id, self._get_routes(logical_id) + + def add_routes(self, logical_id, routes): + """ + Stores the given routes tagged under the given logicalId + Parameters + ---------- + logical_id : str + LogicalId of the AWS::Serverless::Api or AWS::ApiGateway::RestApi resource + routes : list of samcli.commands.local.agiw.local_apigw_service.Route + List of routes available in this resource + """ + self._get_routes(logical_id).extend(routes) + + def _get_routes(self, logical_id): + """ + Returns the properties of resource with given logical ID. If a resource is not found, then it returns an + empty data. + Parameters + ---------- + logical_id : str + Logical ID of the resource + Returns + ------- + samcli.commands.local.lib.Routes + Properties object for this resource. + """ + + return self._route_per_resource[logical_id] + + @property + def routes(self): + return self._routes if self._routes else self.all_routes() + + @routes.setter + def routes(self, routes): + self._routes = routes + + def all_routes(self): + """ + Gets all the routes within the _route_per_resource + + Return + ------- + All the routes within the _route_per_resource + """ + routes = [] + for logical_id in self._route_per_resource.keys(): + routes.extend(self._get_routes(logical_id)) + return routes + + def get_api(self): + """ + Creates the api using the parts from the ApiCollector. The routes are also deduped so that there is no + duplicate routes with the same function name, path, but different method. + + The normalised_routes are the routes that have been processed. By default, this will get all the routes. + However, it can be changed to override the default value of normalised routes such as in SamApiProvider + + Return + ------- + An Api object with all the properties + """ + api = Api() + routes = self.dedupe_function_routes(self.routes) + routes = self.normalize_cors_methods(routes, self.cors) + api.routes = routes + api.binary_media_types_set = self.binary_media_types_set + api.stage_name = self.stage_name + api.stage_variables = self.stage_variables + api.cors = self.cors + return api + + @staticmethod + def normalize_cors_methods(routes, cors): + """ + Adds OPTIONS method to all the route methods if cors exists + + Parameters + ----------- + routes: list(samcli.local.apigw.local_apigw_service.Route) + List of Routes + + cors: samcli.commands.local.lib.provider.Cors + the cors object for the api + + Return + ------- + A list of routes without duplicate routes with the same function_name and method + """ + + def add_options_to_route(route): + if "OPTIONS" not in route.methods: + route.methods.append("OPTIONS") + return route + + return routes if not cors else [add_options_to_route(route) for route in routes] + + @staticmethod + def dedupe_function_routes(routes): + """ + Remove duplicate routes that have the same function_name and method + + route: list(Route) + List of Routes + + Return + ------- + A list of routes without duplicate routes with the same function_name and method + """ + grouped_routes = {} + + for route in routes: + key = "{}-{}".format(route.function_name, route.path) + config = grouped_routes.get(key, None) + methods = route.methods + if config: + methods += config.methods + sorted_methods = sorted(methods) + grouped_routes[key] = Route(function_name=route.function_name, path=route.path, methods=sorted_methods) + return list(grouped_routes.values()) + + def add_binary_media_types(self, logical_id, binary_media_types): + """ + Stores the binary media type configuration for the API with given logical ID + Parameters + ---------- + + logical_id : str + LogicalId of the AWS::Serverless::Api resource + + api: samcli.commands.local.lib.provider.Api + Instance of the Api which will save all the api configurations + + binary_media_types : list of str + List of binary media types supported by this resource + """ + + binary_media_types = binary_media_types or [] + for value in binary_media_types: + normalized_value = self.normalize_binary_media_type(value) + + # If the value is not supported, then just skip it. + if normalized_value: + self.binary_media_types_set.add(normalized_value) + else: + LOG.debug("Unsupported data type of binary media type value of resource '%s'", logical_id) + + @staticmethod + def normalize_binary_media_type(value): + """ + Converts binary media types values to the canonical format. Ex: image~1gif -> image/gif. If the value is not + a string, then this method just returns None + Parameters + ---------- + value : str + Value to be normalized + Returns + ------- + str or None + Normalized value. If the input was not a string, then None is returned + """ + + if not isinstance(value, string_types): + # It is possible that user specified a dict value for one of the binary media types. We just skip them + return None + + return value.replace("~1", "/") diff --git a/samcli/commands/local/lib/api_provider.py b/samcli/commands/local/lib/api_provider.py new file mode 100644 index 0000000000..7a95959e6d --- /dev/null +++ b/samcli/commands/local/lib/api_provider.py @@ -0,0 +1,94 @@ +"""Class that provides the Api with a list of routes from a Template""" + +import logging + +from samcli.commands.local.lib.api_collector import ApiCollector +from samcli.commands.local.lib.cfn_api_provider import CfnApiProvider +from samcli.commands.local.lib.cfn_base_api_provider import CfnBaseApiProvider +from samcli.commands.local.lib.provider import AbstractApiProvider +from samcli.commands.local.lib.sam_api_provider import SamApiProvider +from samcli.commands.local.lib.sam_base_provider import SamBaseProvider + +LOG = logging.getLogger(__name__) + + +class ApiProvider(AbstractApiProvider): + + def __init__(self, template_dict, parameter_overrides=None, cwd=None): + """ + Initialize the class with template data. The template_dict is assumed + to be valid, normalized and a dictionary. template_dict should be normalized by running any and all + pre-processing before passing to this class. + This class does not perform any syntactic validation of the template. + + After the class is initialized, changes to ``template_dict`` will not be reflected in here. + You will need to explicitly update the class with new template, if necessary. + + Parameters + ---------- + template_dict : dict + Template as a dictionary + + cwd : str + Optional working directory with respect to which we will resolve relative path to Swagger file + """ + self.template_dict = SamBaseProvider.get_template(template_dict, parameter_overrides) + self.resources = self.template_dict.get("Resources", {}) + + LOG.debug("%d resources found in the template", len(self.resources)) + + # Store a set of apis + self.cwd = cwd + self.api = self._extract_api(self.resources) + self.routes = self.api.routes + LOG.debug("%d APIs found in the template", len(self.routes)) + + def get_all(self): + """ + Yields all the Apis in the current Provider + + :yields api: an Api object with routes and properties + """ + + yield self.api + + def _extract_api(self, resources): + """ + Extracts all the routes by running through the one providers. The provider that has the first type matched + will be run across all the resources + + Parameters + ---------- + resources: dict + The dictionary containing the different resources within the template + Returns + --------- + An Api from the parsed template + """ + + collector = ApiCollector() + provider = self.find_api_provider(resources) + provider.extract_resources(resources, collector, cwd=self.cwd) + return collector.get_api() + + @staticmethod + def find_api_provider(resources): + """ + Finds the ApiProvider given the first api type of the resource + + Parameters + ----------- + resources: dict + The dictionary containing the different resources within the template + + Return + ---------- + Instance of the ApiProvider that will be run on the template with a default of SamApiProvider + """ + for _, resource in resources.items(): + if resource.get(CfnBaseApiProvider.RESOURCE_TYPE) in SamApiProvider.TYPES: + return SamApiProvider() + elif resource.get(CfnBaseApiProvider.RESOURCE_TYPE) in CfnApiProvider.TYPES: + return CfnApiProvider() + + return SamApiProvider() diff --git a/samcli/commands/local/lib/cfn_api_provider.py b/samcli/commands/local/lib/cfn_api_provider.py new file mode 100644 index 0000000000..fb13450290 --- /dev/null +++ b/samcli/commands/local/lib/cfn_api_provider.py @@ -0,0 +1,218 @@ +"""Parses SAM given a template""" +import logging + +from six import string_types + +from samcli.commands.local.lib.swagger.integration_uri import LambdaUri +from samcli.local.apigw.local_apigw_service import Route +from samcli.commands.local.cli_common.user_exceptions import InvalidSamTemplateException +from samcli.commands.local.lib.cfn_base_api_provider import CfnBaseApiProvider + +LOG = logging.getLogger(__name__) + + +class CfnApiProvider(CfnBaseApiProvider): + APIGATEWAY_RESTAPI = "AWS::ApiGateway::RestApi" + APIGATEWAY_STAGE = "AWS::ApiGateway::Stage" + APIGATEWAY_RESOURCE = "AWS::ApiGateway::Resource" + APIGATEWAY_METHOD = "AWS::ApiGateway::Method" + METHOD_BINARY_TYPE = "CONVERT_TO_BINARY" + TYPES = [ + APIGATEWAY_RESTAPI, + APIGATEWAY_STAGE, + APIGATEWAY_RESOURCE, + APIGATEWAY_METHOD + ] + + def extract_resources(self, resources, collector, cwd=None): + """ + Extract the Route Object from a given resource and adds it to the RouteCollector. + + Parameters + ---------- + resources: dict + The dictionary containing the different resources within the template + + collector: samcli.commands.local.lib.route_collector.RouteCollector + Instance of the API collector that where we will save the API information + + cwd : str + Optional working directory with respect to which we will resolve relative path to Swagger file + + Return + ------- + Returns a list of routes + """ + + for logical_id, resource in resources.items(): + resource_type = resource.get(CfnBaseApiProvider.RESOURCE_TYPE) + if resource_type == CfnApiProvider.APIGATEWAY_RESTAPI: + self._extract_cloud_formation_route(logical_id, resource, collector, cwd=cwd) + + if resource_type == CfnApiProvider.APIGATEWAY_STAGE: + self._extract_cloud_formation_stage(resources, resource, collector) + + if resource_type == CfnApiProvider.APIGATEWAY_METHOD: + self._extract_cloud_formation_method(resources, logical_id, resource, collector) + + all_apis = [] + for _, apis in collector: + all_apis.extend(apis) + return all_apis + + def _extract_cloud_formation_route(self, logical_id, api_resource, collector, cwd=None): + """ + Extract APIs from AWS::ApiGateway::RestApi resource by reading and parsing Swagger documents. The result is + added to the collector. + + Parameters + ---------- + logical_id : str + Logical ID of the resource + + api_resource : dict + Resource definition, including its properties + + collector : ApiCollector + Instance of the API collector that where we will save the API information + """ + properties = api_resource.get("Properties", {}) + body = properties.get("Body") + body_s3_location = properties.get("BodyS3Location") + binary_media = properties.get("BinaryMediaTypes", []) + + if not body and not body_s3_location: + # Swagger is not found anywhere. + LOG.debug("Skipping resource '%s'. Swagger document not found in Body and BodyS3Location", + logical_id) + return + self.extract_swagger_route(logical_id, body, body_s3_location, binary_media, collector, cwd) + + @staticmethod + def _extract_cloud_formation_stage(resources, stage_resource, collector): + """ + Extract the stage from AWS::ApiGateway::Stage resource by reading and adds it to the collector. + Parameters + ---------- + resources: dict + All Resource definition, including its properties + + stage_resource : dict + Stage Resource definition, including its properties + + collector : ApiCollector + Instance of the API collector that where we will save the API information + """ + properties = stage_resource.get("Properties", {}) + stage_name = properties.get("StageName") + stage_variables = properties.get("Variables") + + logical_id = properties.get("RestApiId") + if not logical_id: + raise InvalidSamTemplateException("The AWS::ApiGateway::Stage must have a RestApiId property") + rest_api_resource_type = resources.get(logical_id, {}).get("Type") + if rest_api_resource_type != CfnApiProvider.APIGATEWAY_RESTAPI: + raise InvalidSamTemplateException( + "The AWS::ApiGateway::Stage must have a valid RestApiId that points to RestApi resource {}".format( + logical_id)) + + collector.stage_name = stage_name + collector.stage_variables = stage_variables + + def _extract_cloud_formation_method(self, resources, logical_id, method_resource, collector): + """ + Extract APIs from AWS::ApiGateway::Method and work backwards up the tree to resolve and find the true path. + + Parameters + ---------- + resources: dict + All Resource definition, including its properties + + logical_id : str + Logical ID of the resource + + method_resource : dict + Resource definition, including its properties + + collector : ApiCollector + Instance of the API collector that where we will save the API information + """ + + properties = method_resource.get("Properties", {}) + resource_id = properties.get("ResourceId") + rest_api_id = properties.get("RestApiId") + method = properties.get("HttpMethod") + + resource_path = "/" + if isinstance(resource_id, string_types): # If the resource_id resolves to a string + resource = resources.get(resource_id) + + if resource: + resource_path = self.resolve_resource_path(resources, resource, "") + else: + # This is the case that a raw ref resolves to a string { "Fn::GetAtt": ["MyRestApi", "RootResourceId"] } + resource_path = resource_id + + integration = properties.get("Integration", {}) + content_type = integration.get("ContentType") + + content_handling = integration.get("ContentHandling") + + if content_handling == CfnApiProvider.METHOD_BINARY_TYPE and content_type: + collector.add_binary_media_types(logical_id, [content_type]) + + routes = Route(methods=[method], + function_name=self._get_integration_function_name(integration), + path=resource_path) + collector.add_routes(rest_api_id, [routes]) + + def resolve_resource_path(self, resources, resource, current_path): + """ + Extract path from the Resource object by going up the tree + + Parameters + ---------- + resources: dict + Dictionary containing all the resources to resolve + + resource : dict + AWS::ApiGateway::Resource definition and its properties + + current_path : str + Current path resolved so far + """ + + properties = resource.get("Properties", {}) + parent_id = properties.get("ParentId") + resource_path = properties.get("PathPart") + parent = resources.get(parent_id) + if parent: + return self.resolve_resource_path(resources, parent, "/" + resource_path + current_path) + if parent_id: + return parent_id + resource_path + current_path + + return "/" + resource_path + current_path + + @staticmethod + def _get_integration_function_name(integration): + """ + Tries to parse the Lambda Function name from the Integration defined in the method configuration. Integration + configuration. We care only about Lambda integrations, which are of type aws_proxy, and ignore the rest. + Integration URI is complex and hard to parse. Hence we do our best to extract function name out of + integration URI. If not possible, we return None. + + Parameters + ---------- + method_config : dict + Dictionary containing the method configuration which might contain integration settings + + Returns + ------- + string or None + Lambda function name, if possible. None, if not. + """ + + if integration \ + and isinstance(integration, dict): + # Integration must be "aws_proxy" otherwise we don't care about it + return LambdaUri.get_function_name(integration.get("Uri")) diff --git a/samcli/commands/local/lib/cfn_base_api_provider.py b/samcli/commands/local/lib/cfn_base_api_provider.py new file mode 100644 index 0000000000..8d0d4c3774 --- /dev/null +++ b/samcli/commands/local/lib/cfn_base_api_provider.py @@ -0,0 +1,69 @@ +"""Class that parses the CloudFormation Api Template""" +import logging + +from samcli.commands.local.lib.swagger.parser import SwaggerParser +from samcli.commands.local.lib.swagger.reader import SwaggerReader + +LOG = logging.getLogger(__name__) + + +class CfnBaseApiProvider(object): + RESOURCE_TYPE = "Type" + + def extract_resources(self, resources, collector, cwd=None): + """ + Extract the Route Object from a given resource and adds it to the RouteCollector. + + Parameters + ---------- + resources: dict + The dictionary containing the different resources within the template + + collector: samcli.commands.local.lib.route_collector.RouteCollector + Instance of the API collector that where we will save the API information + + cwd : str + Optional working directory with respect to which we will resolve relative path to Swagger file + + Return + ------- + Returns a list of routes + """ + raise NotImplementedError("not implemented") + + def extract_swagger_route(self, logical_id, body, uri, binary_media, collector, cwd=None): + """ + Parse the Swagger documents and adds it to the ApiCollector. + + Parameters + ---------- + logical_id : str + Logical ID of the resource + + body : dict + The body of the RestApi + + uri : str or dict + The url to location of the RestApi + + binary_media: list + The link to the binary media + + collector: samcli.commands.local.lib.route_collector.RouteCollector + Instance of the Route collector that where we will save the route information + + cwd : str + Optional working directory with respect to which we will resolve relative path to Swagger file + """ + reader = SwaggerReader(definition_body=body, + definition_uri=uri, + working_dir=cwd) + swagger = reader.read() + parser = SwaggerParser(swagger) + routes = parser.get_routes() + LOG.debug("Found '%s' APIs in resource '%s'", len(routes), logical_id) + + collector.add_routes(logical_id, routes) + + collector.add_binary_media_types(logical_id, parser.get_binary_media_types()) # Binary media from swagger + collector.add_binary_media_types(logical_id, binary_media) # Binary media specified on resource in template diff --git a/samcli/commands/local/lib/local_api_service.py b/samcli/commands/local/lib/local_api_service.py index d0ebbbd975..441d6c3cbc 100644 --- a/samcli/commands/local/lib/local_api_service.py +++ b/samcli/commands/local/lib/local_api_service.py @@ -2,20 +2,20 @@ Connects the CLI with Local API Gateway service. """ -import os import logging +import os -from samcli.local.apigw.local_apigw_service import LocalApigwService, Route -from samcli.commands.local.lib.sam_api_provider import SamApiProvider from samcli.commands.local.lib.exceptions import NoApisDefined +from samcli.local.apigw.local_apigw_service import LocalApigwService +from samcli.commands.local.lib.api_provider import ApiProvider LOG = logging.getLogger(__name__) class LocalApiService(object): """ - Implementation of Local API service that is capable of serving APIs defined in a SAM file that invoke a Lambda - function. + Implementation of Local API service that is capable of serving API defined in a configuration file that invoke a + Lambda function. """ def __init__(self, @@ -38,9 +38,9 @@ def __init__(self, self.static_dir = static_dir self.cwd = lambda_invoke_context.get_cwd() - self.api_provider = SamApiProvider(lambda_invoke_context.template, - parameter_overrides=lambda_invoke_context.parameter_overrides, - cwd=self.cwd) + self.api_provider = ApiProvider(lambda_invoke_context.template, + parameter_overrides=lambda_invoke_context.parameter_overrides, + cwd=self.cwd) self.lambda_runner = lambda_invoke_context.local_lambda_runner self.stderr_stream = lambda_invoke_context.stderr @@ -53,10 +53,8 @@ def start(self): NOTE: This is a blocking call that will not return until the thread is interrupted with SIGINT/SIGTERM """ - routing_list = self._make_routing_list(self.api_provider) - - if not routing_list: - raise NoApisDefined("No APIs available in SAM template") + if not self.api_provider.api.routes: + raise NoApisDefined("No APIs available in template") static_dir_path = self._make_static_dir_path(self.cwd, self.static_dir) @@ -64,7 +62,7 @@ def start(self): # contains the response to the API which is sent out as HTTP response. Only stderr needs to be printed # to the console or a log file. stderr from Docker container contains runtime logs and output of print # statements from the Lambda function - service = LocalApigwService(routing_list=routing_list, + service = LocalApigwService(api=self.api_provider.api, lambda_runner=self.lambda_runner, static_dir=static_dir_path, port=self.port, @@ -74,7 +72,7 @@ def start(self): service.create() # Print out the list of routes that will be mounted - self._print_routes(self.api_provider, self.host, self.port) + self._print_routes(self.api_provider.api.routes, self.host, self.port) LOG.info("You can now browse to the above endpoints to invoke your functions. " "You do not need to restart/reload SAM CLI while working on your functions, " "changes will be reflected instantly/automatically. You only need to restart " @@ -83,30 +81,7 @@ def start(self): service.run() @staticmethod - def _make_routing_list(api_provider): - """ - Returns a list of routes to configure the Local API Service based on the APIs configured in the template. - - Parameters - ---------- - api_provider : samcli.commands.local.lib.sam_api_provider.SamApiProvider - - Returns - ------- - list(samcli.local.apigw.service.Route) - List of Routes to pass to the service - """ - - routes = [] - for api in api_provider.get_all(): - route = Route(methods=[api.method], function_name=api.function_name, path=api.path, - binary_types=api.binary_media_types, stage_name=api.stage_name, - stage_variables=api.stage_variables) - routes.append(route) - return routes - - @staticmethod - def _print_routes(api_provider, host, port): + def _print_routes(routes, host, port): """ Helper method to print the APIs that will be mounted. This method is purely for printing purposes. This method takes in a list of Route Configurations and prints out the Routes grouped by path. @@ -116,33 +91,24 @@ def _print_routes(api_provider, host, port): Mounting Product at http://127.0.0.1:3000/path1/bar [GET, POST, DELETE] Mounting Product at http://127.0.0.1:3000/path2/bar [HEAD] - :param samcli.commands.local.lib.provider.ApiProvider api_provider: API Provider that can return a list of APIs - :param string host: Host name where the service is running - :param int port: Port number where the service is running - :returns list(string): List of lines that were printed to the console. Helps with testing + :param list(Route) routes: + List of routes grouped by the same function_name and path + :param string host: + Host name where the service is running + :param int port: + Port number where the service is running + :returns list(string): + List of lines that were printed to the console. Helps with testing """ - grouped_api_configs = {} - - for api in api_provider.get_all(): - key = "{}-{}".format(api.function_name, api.path) - - config = grouped_api_configs.get(key, {}) - config.setdefault("methods", []) - - config["function_name"] = api.function_name - config["path"] = api.path - config["methods"].append(api.method) - - grouped_api_configs[key] = config print_lines = [] - for _, config in grouped_api_configs.items(): - methods_str = "[{}]".format(', '.join(config["methods"])) + for route in routes: + methods_str = "[{}]".format(', '.join(route.methods)) output = "Mounting {} at http://{}:{}{} {}".format( - config["function_name"], + route.function_name, host, port, - config["path"], + route.path, methods_str) print_lines.append(output) diff --git a/samcli/commands/local/lib/provider.py b/samcli/commands/local/lib/provider.py index eead981089..a61891dbfc 100644 --- a/samcli/commands/local/lib/provider.py +++ b/samcli/commands/local/lib/provider.py @@ -199,46 +199,72 @@ def get_all(self): raise NotImplementedError("not implemented") -_ApiTuple = namedtuple("Api", [ +class Api(object): + def __init__(self, routes=None): + if routes is None: + routes = [] + self.routes = routes - # String. Path that this API serves. Ex: /foo, /bar/baz - "path", + # Optional Dictionary containing CORS configuration on this path+method If this configuration is set, + # then API server will automatically respond to OPTIONS HTTP method on this path and respond with appropriate + # CORS headers based on configuration. - # String. HTTP Method this API responds with - "method", + self.cors = None + # If this configuration is set, then API server will automatically respond to OPTIONS HTTP method on this + # path and - # String. Name of the Function this API connects to - "function_name", + self.binary_media_types_set = set() - # Optional Dictionary containing CORS configuration on this path+method - # If this configuration is set, then API server will automatically respond to OPTIONS HTTP method on this path and - # respond with appropriate CORS headers based on configuration. - "cors", + self.stage_name = None + self.stage_variables = None - # List(Str). List of the binary media types the API - "binary_media_types", - # The Api stage name - "stage_name", - # The variables for that stage - "stage_variables" -]) -_ApiTuple.__new__.__defaults__ = (None, # Cors is optional and defaults to None - [], # binary_media_types is optional and defaults to empty, - None, # Stage name is optional with default None - None # Stage variables is optional with default None - ) - - -class Api(_ApiTuple): def __hash__(self): # Other properties are not a part of the hash - return hash(self.path) * hash(self.method) * hash(self.function_name) + return hash(self.routes) * hash(self.cors) * hash(self.binary_media_types_set) + + @property + def binary_media_types(self): + return list(self.binary_media_types_set) + + +_CorsTuple = namedtuple("Cors", ["allow_origin", "allow_methods", "allow_headers", "max_age"]) + +_CorsTuple.__new__.__defaults__ = (None, # Allow Origin defaults to None + None, # Allow Methods is optional and defaults to empty + None, # Allow Headers is optional and defaults to empty + None # MaxAge is optional and defaults to empty + ) -Cors = namedtuple("Cors", ["AllowOrigin", "AllowMethods", "AllowHeaders"]) +class Cors(_CorsTuple): -class ApiProvider(object): + @staticmethod + def cors_to_headers(cors): + """ + Convert CORS object to headers dictionary + Parameters + ---------- + cors list(samcli.commands.local.lib.provider.Cors) + CORS configuration objcet + Returns + ------- + Dictionary with CORS headers + """ + if not cors: + return {} + headers = { + 'Access-Control-Allow-Origin': cors.allow_origin, + 'Access-Control-Allow-Methods': cors.allow_methods, + 'Access-Control-Allow-Headers': cors.allow_headers, + 'Access-Control-Max-Age': cors.max_age + } + # Filters out items in the headers dictionary that isn't empty. + # This is required because the flask Headers dict will send an invalid 'None' string + return {h_key: h_value for h_key, h_value in headers.items() if h_value is not None} + + +class AbstractApiProvider(object): """ Abstract base class to return APIs and the functions they route to """ diff --git a/samcli/commands/local/lib/sam_api_provider.py b/samcli/commands/local/lib/sam_api_provider.py index 84336a8d2d..9fda35a934 100644 --- a/samcli/commands/local/lib/sam_api_provider.py +++ b/samcli/commands/local/lib/sam_api_provider.py @@ -1,111 +1,61 @@ -"""Class that provides Apis from a SAM Template""" +"""Parses SAM given the template""" import logging -from collections import namedtuple from six import string_types -from samcli.commands.local.lib.swagger.parser import SwaggerParser -from samcli.commands.local.lib.provider import ApiProvider, Api -from samcli.commands.local.lib.sam_base_provider import SamBaseProvider -from samcli.commands.local.lib.swagger.reader import SamSwaggerReader +from samcli.commands.local.lib.provider import Cors +from samcli.commands.local.lib.cfn_base_api_provider import CfnBaseApiProvider from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException +from samcli.local.apigw.local_apigw_service import Route LOG = logging.getLogger(__name__) -class SamApiProvider(ApiProvider): - _IMPLICIT_API_RESOURCE_ID = "ServerlessRestApi" - _SERVERLESS_FUNCTION = "AWS::Serverless::Function" - _SERVERLESS_API = "AWS::Serverless::Api" - _TYPE = "Type" - +class SamApiProvider(CfnBaseApiProvider): + SERVERLESS_FUNCTION = "AWS::Serverless::Function" + SERVERLESS_API = "AWS::Serverless::Api" + TYPES = [ + SERVERLESS_FUNCTION, + SERVERLESS_API + ] _FUNCTION_EVENT_TYPE_API = "Api" _FUNCTION_EVENT = "Events" _EVENT_PATH = "Path" _EVENT_METHOD = "Method" + _EVENT_TYPE = "Type" + IMPLICIT_API_RESOURCE_ID = "ServerlessRestApi" - _ANY_HTTP_METHODS = ["GET", - "DELETE", - "PUT", - "POST", - "HEAD", - "OPTIONS", - "PATCH"] - - def __init__(self, template_dict, parameter_overrides=None, cwd=None): + def extract_resources(self, resources, collector, cwd=None): """ - Initialize the class with SAM template data. The template_dict (SAM Templated) is assumed - to be valid, normalized and a dictionary. template_dict should be normalized by running any and all - pre-processing before passing to this class. - This class does not perform any syntactic validation of the template. - - After the class is initialized, changes to ``template_dict`` will not be reflected in here. - You will need to explicitly update the class with new template, if necessary. + Extract the Route Object from a given resource and adds it to the RouteCollector. Parameters ---------- - template_dict : dict - SAM Template as a dictionary - cwd : str - Optional working directory with respect to which we will resolve relative path to Swagger file - """ + resources: dict + The dictionary containing the different resources within the template - self.template_dict = SamBaseProvider.get_template(template_dict, parameter_overrides) - self.resources = self.template_dict.get("Resources", {}) - - LOG.debug("%d resources found in the template", len(self.resources)) - - # Store a set of apis - self.cwd = cwd - self.apis = self._extract_apis(self.resources) - - LOG.debug("%d APIs found in the template", len(self.apis)) - - def get_all(self): - """ - Yields all the Lambda functions with Api Events available in the SAM Template. - - :yields Api: namedtuple containing the Api information - """ - - for api in self.apis: - yield api + collector: samcli.commands.local.lib.route_collector.ApiCollector + Instance of the API collector that where we will save the API information - def _extract_apis(self, resources): - """ - Extract all Implicit Apis (Apis defined through Serverless Function with an Api Event + cwd : str + Optional working directory with respect to which we will resolve relative path to Swagger file - :param dict resources: Dictionary of SAM/CloudFormation resources - :return: List of nametuple Api """ - - # Some properties like BinaryMediaTypes, Cors are set once on the resource but need to be applied to each API. - # For Implicit APIs, which are defined on the Function resource, these properties - # are defined on a AWS::Serverless::Api resource with logical ID "ServerlessRestApi". Therefore, no matter - # if it is an implicit API or an explicit API, there is a corresponding resource of type AWS::Serverless::Api - # that contains these additional configurations. - # - # We use this assumption in the following loop to collect information from resources of type - # AWS::Serverless::Api. We also extract API from Serverless::Function resource and add them to the - # corresponding Serverless::Api resource. This is all done using the ``collector``. - - collector = ApiCollector() - + # AWS::Serverless::Function is currently included when parsing of Apis because when SamBaseProvider is run on + # the template we are creating the implicit apis due to plugins that translate it in the SAM repo, + # which we later merge with the explicit ones in SamApiProvider.merge_apis. This requires the code to be + # parsed here and in InvokeContext. for logical_id, resource in resources.items(): + resource_type = resource.get(CfnBaseApiProvider.RESOURCE_TYPE) + if resource_type == SamApiProvider.SERVERLESS_FUNCTION: + self._extract_routes_from_function(logical_id, resource, collector) + if resource_type == SamApiProvider.SERVERLESS_API: + self._extract_from_serverless_api(logical_id, resource, collector, cwd=cwd) - resource_type = resource.get(SamApiProvider._TYPE) + collector.routes = self.merge_routes(collector) - if resource_type == SamApiProvider._SERVERLESS_FUNCTION: - self._extract_apis_from_function(logical_id, resource, collector) - - if resource_type == SamApiProvider._SERVERLESS_API: - self._extract_from_serverless_api(logical_id, resource, collector) - - apis = SamApiProvider._merge_apis(collector) - return self._normalize_apis(apis) - - def _extract_from_serverless_api(self, logical_id, api_resource, collector): + def _extract_from_serverless_api(self, logical_id, api_resource, collector, cwd=None): """ Extract APIs from AWS::Serverless::Api resource by reading and parsing Swagger documents. The result is added to the collector. @@ -118,138 +68,112 @@ def _extract_from_serverless_api(self, logical_id, api_resource, collector): api_resource : dict Resource definition, including its properties - collector : ApiCollector + collector: samcli.commands.local.lib.route_collector.RouteCollector Instance of the API collector that where we will save the API information + + cwd : str + Optional working directory with respect to which we will resolve relative path to Swagger file + """ properties = api_resource.get("Properties", {}) body = properties.get("DefinitionBody") uri = properties.get("DefinitionUri") binary_media = properties.get("BinaryMediaTypes", []) + cors = self.extract_cors(properties.get("Cors", {})) stage_name = properties.get("StageName") stage_variables = properties.get("Variables") - if not body and not uri: # Swagger is not found anywhere. LOG.debug("Skipping resource '%s'. Swagger document not found in DefinitionBody and DefinitionUri", logical_id) return + self.extract_swagger_route(logical_id, body, uri, binary_media, collector, cwd=cwd) + collector.stage_name = stage_name + collector.stage_variables = stage_variables + collector.cors = cors - reader = SamSwaggerReader(definition_body=body, - definition_uri=uri, - working_dir=self.cwd) - swagger = reader.read() - parser = SwaggerParser(swagger) - apis = parser.get_apis() - LOG.debug("Found '%s' APIs in resource '%s'", len(apis), logical_id) - - collector.add_apis(logical_id, apis) - collector.add_binary_media_types(logical_id, parser.get_binary_media_types()) # Binary media from swagger - collector.add_binary_media_types(logical_id, binary_media) # Binary media specified on resource in template - - collector.add_stage_name(logical_id, stage_name) - collector.add_stage_variables(logical_id, stage_variables) - - @staticmethod - def _merge_apis(collector): + def extract_cors(self, cors_prop): """ - Quite often, an API is defined both in Implicit and Explicit API definitions. In such cases, Implicit API - definition wins because that conveys clear intent that the API is backed by a function. This method will - merge two such list of Apis with the right order of precedence. If a Path+Method combination is defined - in both the places, only one wins. + Extract Cors property from AWS::Serverless::Api resource by reading and parsing Swagger documents. The result + is added to the Api. Parameters ---------- - collector : ApiCollector - Collector object that holds all the APIs specified in the template - - Returns - ------- - list of samcli.commands.local.lib.provider.Api - List of APIs obtained by combining both the input lists. - """ - - implicit_apis = [] - explicit_apis = [] - - # Store implicit and explicit APIs separately in order to merge them later in the correct order - # Implicit APIs are defined on a resource with logicalID ServerlessRestApi - for logical_id, apis in collector: - if logical_id == SamApiProvider._IMPLICIT_API_RESOURCE_ID: - implicit_apis.extend(apis) - else: - explicit_apis.extend(apis) - - # We will use "path+method" combination as key to this dictionary and store the Api config for this combination. - # If an path+method combo already exists, then overwrite it if and only if this is an implicit API - all_apis = {} - - # By adding implicit APIs to the end of the list, they will be iterated last. If a configuration was already - # written by explicit API, it will be overriden by implicit API, just by virtue of order of iteration. - all_configs = explicit_apis + implicit_apis - - for config in all_configs: - # Normalize the methods before de-duping to allow an ANY method in implicit API to override a regular HTTP - # method on explicit API. - for normalized_method in SamApiProvider._normalize_http_methods(config.method): - key = config.path + normalized_method - all_apis[key] = config - - result = set(all_apis.values()) # Assign to a set() to de-dupe - LOG.debug("Removed duplicates from '%d' Explicit APIs and '%d' Implicit APIs to produce '%d' APIs", - len(explicit_apis), len(implicit_apis), len(result)) - - return list(result) + cors_prop : dict + Resource properties for Cors + """ + cors = None + if cors_prop and isinstance(cors_prop, dict): + allow_methods = cors_prop.get("AllowMethods", ','.join(sorted(Route.ANY_HTTP_METHODS))) + allow_methods = self.normalize_cors_allow_methods(allow_methods) + cors = Cors( + allow_origin=cors_prop.get("AllowOrigin"), + allow_methods=allow_methods, + allow_headers=cors_prop.get("AllowHeaders"), + max_age=cors_prop.get("MaxAge") + ) + elif cors_prop and isinstance(cors_prop, string_types): + cors = Cors( + allow_origin=cors_prop, + allow_methods=','.join(sorted(Route.ANY_HTTP_METHODS)), + allow_headers=None, + max_age=None + ) + return cors @staticmethod - def _normalize_apis(apis): + def normalize_cors_allow_methods(allow_methods): """ - Normalize the APIs to use standard method name + Normalize cors AllowMethods and Options to the methods if it's missing. Parameters ---------- - apis : list of samcli.commands.local.lib.provider.Api - List of APIs to replace normalize + allow_methods : str + The allow_methods string provided in the query - Returns + Return ------- - list of samcli.commands.local.lib.provider.Api - List of normalized APIs + A string with normalized route """ + if allow_methods == "*": + return ','.join(sorted(Route.ANY_HTTP_METHODS)) + methods = allow_methods.split(",") + normalized_methods = [] + for method in methods: + normalized_method = method.strip().upper() + if normalized_method not in Route.ANY_HTTP_METHODS: + raise InvalidSamDocumentException("The method {} is not a valid CORS method".format(normalized_method)) + normalized_methods.append(normalized_method) - result = list() - for api in apis: - for normalized_method in SamApiProvider._normalize_http_methods(api.method): - # _replace returns a copy of the namedtuple. This is the official way of creating copies of namedtuple - result.append(api._replace(method=normalized_method)) + if "OPTIONS" not in normalized_methods: + normalized_methods.append("OPTIONS") - return result + return ','.join(sorted(normalized_methods)) - @staticmethod - def _extract_apis_from_function(logical_id, function_resource, collector): + def _extract_routes_from_function(self, logical_id, function_resource, collector): """ - Fetches a list of APIs configured for this SAM Function resource. + Fetches a list of routes configured for this SAM Function resource. Parameters ---------- logical_id : str - Logical ID of the resource + Logical ID of the resourc function_resource : dict Contents of the function resource including its properties - collector : ApiCollector + collector: samcli.commands.local.lib.route_collector.RouteCollector Instance of the API collector that where we will save the API information """ resource_properties = function_resource.get("Properties", {}) - serverless_function_events = resource_properties.get(SamApiProvider._FUNCTION_EVENT, {}) - SamApiProvider._extract_apis_from_events(logical_id, serverless_function_events, collector) + serverless_function_events = resource_properties.get(self._FUNCTION_EVENT, {}) + self.extract_routes_from_events(logical_id, serverless_function_events, collector) - @staticmethod - def _extract_apis_from_events(function_logical_id, serverless_function_events, collector): + def extract_routes_from_events(self, function_logical_id, serverless_function_events, collector): """ - Given an AWS::Serverless::Function Event Dictionary, extract out all 'Api' events and store within the + Given an AWS::Serverless::Function Event Dictionary, extract out all 'route' events and store within the collector Parameters @@ -260,27 +184,27 @@ def _extract_apis_from_events(function_logical_id, serverless_function_events, c serverless_function_events : dict Event Dictionary of a AWS::Serverless::Function - collector : ApiCollector - Instance of the API collector that where we will save the API information + collector: samcli.commands.local.lib.route_collector.RouteCollector + Instance of the Route collector that where we will save the route information """ count = 0 for _, event in serverless_function_events.items(): - if SamApiProvider._FUNCTION_EVENT_TYPE_API == event.get(SamApiProvider._TYPE): - api_resource_id, api = SamApiProvider._convert_event_api(function_logical_id, event.get("Properties")) - collector.add_apis(api_resource_id, [api]) + if self._FUNCTION_EVENT_TYPE_API == event.get(self._EVENT_TYPE): + route_resource_id, route = self._convert_event_route(function_logical_id, event.get("Properties")) + collector.add_routes(route_resource_id, [route]) count += 1 LOG.debug("Found '%d' API Events in Serverless function with name '%s'", count, function_logical_id) @staticmethod - def _convert_event_api(lambda_logical_id, event_properties): + def _convert_event_route(lambda_logical_id, event_properties): """ - Converts a AWS::Serverless::Function's Event Property to an Api configuration usable by the provider. + Converts a AWS::Serverless::Function's Event Property to an Route configuration usable by the provider. :param str lambda_logical_id: Logical Id of the AWS::Serverless::Function :param dict event_properties: Dictionary of the Event's Property - :return tuple: tuple of API resource name and Api namedTuple + :return tuple: tuple of route resource name and route """ path = event_properties.get(SamApiProvider._EVENT_PATH) method = event_properties.get(SamApiProvider._EVENT_METHOD) @@ -288,7 +212,7 @@ def _convert_event_api(lambda_logical_id, event_properties): # An API Event, can have RestApiId property which designates the resource that owns this API. If omitted, # the API is owned by Implicit API resource. This could either be a direct resource logical ID or a # "Ref" of the logicalID - api_resource_id = event_properties.get("RestApiId", SamApiProvider._IMPLICIT_API_RESOURCE_ID) + api_resource_id = event_properties.get("RestApiId", SamApiProvider.IMPLICIT_API_RESOURCE_ID) if isinstance(api_resource_id, dict) and "Ref" in api_resource_id: api_resource_id = api_resource_id["Ref"] @@ -299,229 +223,54 @@ def _convert_event_api(lambda_logical_id, event_properties): "It should either be a LogicalId string or a Ref of a Logical Id string" .format(lambda_logical_id)) - return api_resource_id, Api(path=path, method=method, function_name=lambda_logical_id) + return api_resource_id, Route(path=path, methods=[method], function_name=lambda_logical_id) @staticmethod - def _normalize_http_methods(http_method): - """ - Normalizes Http Methods. Api Gateway allows a Http Methods of ANY. This is a special verb to denote all - supported Http Methods on Api Gateway. - - :param str http_method: Http method - :yield str: Either the input http_method or one of the _ANY_HTTP_METHODS (normalized Http Methods) - """ - - if http_method.upper() == 'ANY': - for method in SamApiProvider._ANY_HTTP_METHODS: - yield method.upper() - else: - yield http_method.upper() - - -class ApiCollector(object): - """ - Class to store the API configurations in the SAM Template. This class helps store both implicit and explicit - APIs in a standardized format - """ - - # Properties of each API. The structure is quite similar to the properties of AWS::Serverless::Api resource. - # This is intentional because it allows us to easily extend this class to support future properties on the API. - # We will store properties of Implicit APIs also in this format which converges the handling of implicit & explicit - # APIs. - Properties = namedtuple("Properties", ["apis", "binary_media_types", "cors", "stage_name", "stage_variables"]) - - def __init__(self): - # API properties stored per resource. Key is the LogicalId of the AWS::Serverless::Api resource and - # value is the properties - self.by_resource = {} - - def __iter__(self): + def merge_routes(collector): """ - Iterator to iterate through all the APIs stored in the collector. In each iteration, this yields the - LogicalId of the API resource and a list of APIs available in this resource. - - Yields - ------- - str - LogicalID of the AWS::Serverless::Api resource - list samcli.commands.local.lib.provider.Api - List of the API available in this resource along with additional configuration like binary media types. - """ - - for logical_id, _ in self.by_resource.items(): - yield logical_id, self._get_apis_with_config(logical_id) - - def add_apis(self, logical_id, apis): - """ - Stores the given APIs tagged under the given logicalId - - Parameters - ---------- - logical_id : str - LogicalId of the AWS::Serverless::Api resource - - apis : list of samcli.commands.local.lib.provider.Api - List of APIs available in this resource - """ - properties = self._get_properties(logical_id) - properties.apis.extend(apis) - - def add_binary_media_types(self, logical_id, binary_media_types): - """ - Stores the binary media type configuration for the API with given logical ID - - Parameters - ---------- - logical_id : str - LogicalId of the AWS::Serverless::Api resource - - binary_media_types : list of str - List of binary media types supported by this resource - - """ - properties = self._get_properties(logical_id) - - binary_media_types = binary_media_types or [] - for value in binary_media_types: - normalized_value = self._normalize_binary_media_type(value) - - # If the value is not supported, then just skip it. - if normalized_value: - properties.binary_media_types.add(normalized_value) - else: - LOG.debug("Unsupported data type of binary media type value of resource '%s'", logical_id) - - def add_stage_name(self, logical_id, stage_name): - """ - Stores the stage name for the API with the given local ID - - Parameters - ---------- - logical_id : str - LogicalId of the AWS::Serverless::Api resource - - stage_name : str - The stage_name string - - """ - properties = self._get_properties(logical_id) - properties = properties._replace(stage_name=stage_name) - self._set_properties(logical_id, properties) - - def add_stage_variables(self, logical_id, stage_variables): - """ - Stores the stage variables for the API with the given local ID - - Parameters - ---------- - logical_id : str - LogicalId of the AWS::Serverless::Api resource - - stage_variables : dict - A dictionary containing stage variables. - - """ - properties = self._get_properties(logical_id) - properties = properties._replace(stage_variables=stage_variables) - self._set_properties(logical_id, properties) - - def _get_apis_with_config(self, logical_id): - """ - Returns the list of APIs in this resource along with other extra configuration such as binary media types, - cors etc. Additional configuration is merged directly into the API data because these properties, although - defined globally, actually apply to each API. - - Parameters - ---------- - logical_id : str - Logical ID of the resource to fetch data for - - Returns - ------- - list of samcli.commands.local.lib.provider.Api - List of APIs with additional configurations for the resource with given logicalId. If there are no APIs, - then it returns an empty list - """ - - properties = self._get_properties(logical_id) - - # These configs need to be applied to each API - binary_media = sorted(list(properties.binary_media_types)) # Also sort the list to keep the ordering stable - cors = properties.cors - stage_name = properties.stage_name - stage_variables = properties.stage_variables - - result = [] - for api in properties.apis: - # Create a copy of the API with updated configuration - updated_api = api._replace(binary_media_types=binary_media, - cors=cors, - stage_name=stage_name, - stage_variables=stage_variables) - result.append(updated_api) - - return result - - def _get_properties(self, logical_id): - """ - Returns the properties of resource with given logical ID. If a resource is not found, then it returns an - empty data. + Quite often, an API is defined both in Implicit and Explicit Route definitions. In such cases, Implicit API + definition wins because that conveys clear intent that the API is backed by a function. This method will + merge two such list of routes with the right order of precedence. If a Path+Method combination is defined + in both the places, only one wins. Parameters ---------- - logical_id : str - Logical ID of the resource + collector: samcli.commands.local.lib.route_collector.RouteCollector + Collector object that holds all the APIs specified in the template Returns ------- - samcli.commands.local.lib.sam_api_provider.ApiCollector.Properties - Properties object for this resource. - """ - - if logical_id not in self.by_resource: - self.by_resource[logical_id] = self.Properties(apis=[], - # Use a set() to be able to easily de-dupe - binary_media_types=set(), - cors=None, - stage_name=None, - stage_variables=None) - - return self.by_resource[logical_id] - - def _set_properties(self, logical_id, properties): - """ - Sets the properties of resource with given logical ID. If a resource is not found, it does nothing - - Parameters - ---------- - logical_id : str - Logical ID of the resource - properties : samcli.commands.local.lib.sam_api_provider.ApiCollector.Properties - Properties object for this resource. + list of samcli.local.apigw.local_apigw_service.Route + List of routes obtained by combining both the input lists. """ - if logical_id in self.by_resource: - self.by_resource[logical_id] = properties + implicit_routes = [] + explicit_routes = [] - @staticmethod - def _normalize_binary_media_type(value): - """ - Converts binary media types values to the canonical format. Ex: image~1gif -> image/gif. If the value is not - a string, then this method just returns None + # Store implicit and explicit APIs separately in order to merge them later in the correct order + # Implicit APIs are defined on a resource with logicalID ServerlessRestApi + for logical_id, apis in collector: + if logical_id == SamApiProvider.IMPLICIT_API_RESOURCE_ID: + implicit_routes.extend(apis) + else: + explicit_routes.extend(apis) - Parameters - ---------- - value : str - Value to be normalized + # We will use "path+method" combination as key to this dictionary and store the Api config for this combination. + # If an path+method combo already exists, then overwrite it if and only if this is an implicit API + all_routes = {} - Returns - ------- - str or None - Normalized value. If the input was not a string, then None is returned - """ + # By adding implicit APIs to the end of the list, they will be iterated last. If a configuration was already + # written by explicit API, it will be overriden by implicit API, just by virtue of order of iteration. + all_configs = explicit_routes + implicit_routes - if not isinstance(value, string_types): - # It is possible that user specified a dict value for one of the binary media types. We just skip them - return None + for config in all_configs: + # Normalize the methods before de-duping to allow an ANY method in implicit API to override a regular HTTP + # method on explicit route. + for normalized_method in config.methods: + key = config.path + normalized_method + all_routes[key] = config - return value.replace("~1", "/") + result = set(all_routes.values()) # Assign to a set() to de-dupe + LOG.debug("Removed duplicates from '%d' Explicit APIs and '%d' Implicit APIs to produce '%d' APIs", + len(explicit_routes), len(implicit_routes), len(result)) + return list(result) diff --git a/samcli/commands/local/lib/sam_base_provider.py b/samcli/commands/local/lib/sam_base_provider.py index bbf4d6381b..897fa06c56 100644 --- a/samcli/commands/local/lib/sam_base_provider.py +++ b/samcli/commands/local/lib/sam_base_provider.py @@ -4,12 +4,10 @@ import logging -from samtranslator.intrinsics.resolver import IntrinsicsResolver -from samtranslator.intrinsics.actions import RefAction - -from samcli.lib.samlib.wrapper import SamTranslatorWrapper +from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver +from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable from samcli.lib.samlib.resource_metadata_normalizer import ResourceMetadataNormalizer - +from samcli.lib.samlib.wrapper import SamTranslatorWrapper LOG = logging.getLogger(__name__) @@ -19,24 +17,6 @@ class SamBaseProvider(object): Base class for SAM Template providers """ - # There is not much benefit in infering real values for these parameters in local development context. These values - # are usually representative of an AWS environment and stack, but in local development scenario they don't make - # sense. If customers choose to, they can always override this value through the CLI interface. - _DEFAULT_PSEUDO_PARAM_VALUES = { - "AWS::AccountId": "123456789012", - "AWS::Partition": "aws", - - "AWS::Region": "us-east-1", - - "AWS::StackName": "local", - "AWS::StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/" - "local/51af3dc0-da77-11e4-872e-1234567db123", - "AWS::URLSuffix": "localhost" - } - - # Only Ref is supported when resolving template parameters - _SUPPORTED_INTRINSICS = [RefAction] - @staticmethod def get_template(template_dict, parameter_overrides=None): """ @@ -56,42 +36,23 @@ def get_template(template_dict, parameter_overrides=None): dict Processed SAM template """ - template_dict = template_dict or {} if template_dict: template_dict = SamTranslatorWrapper(template_dict).run_plugins() - - template_dict = SamBaseProvider._resolve_parameters(template_dict, parameter_overrides) ResourceMetadataNormalizer.normalize(template_dict) + logical_id_translator = SamBaseProvider._get_parameter_values( + template_dict, parameter_overrides + ) + + resolver = IntrinsicResolver( + template=template_dict, + symbol_resolver=IntrinsicsSymbolTable( + logical_id_translator=logical_id_translator, template=template_dict + ), + ) + template_dict = resolver.resolve_template(ignore_errors=True) return template_dict - @staticmethod - def _resolve_parameters(template_dict, parameter_overrides): - """ - In the given template, apply parameter values to resolve intrinsic functions - - Parameters - ---------- - template_dict : dict - SAM Template - - parameter_overrides : dict - Values for template parameters provided by user - - Returns - ------- - dict - Resolved SAM template - """ - - parameter_values = SamBaseProvider._get_parameter_values(template_dict, parameter_overrides) - - supported_intrinsics = {action.intrinsic_name: action() for action in SamBaseProvider._SUPPORTED_INTRINSICS} - - # Intrinsics resolver will mutate the original template - return IntrinsicsResolver(parameters=parameter_values, supported_intrinsics=supported_intrinsics)\ - .resolve_parameter_refs(template_dict) - @staticmethod def _get_parameter_values(template_dict, parameter_overrides): """ @@ -117,7 +78,7 @@ def _get_parameter_values(template_dict, parameter_overrides): # NOTE: Ordering of following statements is important. It makes sure that any user-supplied values # override the defaults parameter_values = {} - parameter_values.update(SamBaseProvider._DEFAULT_PSEUDO_PARAM_VALUES) + parameter_values.update(IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES) parameter_values.update(default_values) parameter_values.update(parameter_overrides or {}) diff --git a/samcli/commands/local/lib/swagger/parser.py b/samcli/commands/local/lib/swagger/parser.py index 076161993c..072e71c378 100644 --- a/samcli/commands/local/lib/swagger/parser.py +++ b/samcli/commands/local/lib/swagger/parser.py @@ -2,8 +2,8 @@ import logging -from samcli.commands.local.lib.provider import Api from samcli.commands.local.lib.swagger.integration_uri import LambdaUri, IntegrationType +from samcli.local.apigw.local_apigw_service import Route LOG = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def get_binary_media_types(self): """ return self.swagger.get(self._BINARY_MEDIA_TYPES_EXTENSION_KEY) or [] - def get_apis(self): + def get_routes(self): """ Parses a swagger document and returns a list of APIs configured in the document. @@ -62,15 +62,13 @@ def get_apis(self): Returns ------- - list of samcli.commands.local.lib.provider.Api + list of list of samcli.commands.local.apigw.local_apigw_service.Route List of APIs that are configured in the Swagger document """ result = [] paths_dict = self.swagger.get("paths", {}) - binary_media_types = self.get_binary_media_types() - for full_path, path_config in paths_dict.items(): for method, method_config in path_config.items(): @@ -83,11 +81,8 @@ def get_apis(self): if method.lower() == self._ANY_METHOD_EXTENSION_KEY: # Convert to a more commonly used method notation method = self._ANY_METHOD - - api = Api(path=full_path, method=method, function_name=function_name, cors=None, - binary_media_types=binary_media_types) - result.append(api) - + route = Route(function_name, full_path, methods=[method]) + result.append(route) return result def _get_integration_function_name(self, method_config): diff --git a/samcli/commands/local/lib/swagger/reader.py b/samcli/commands/local/lib/swagger/reader.py index d3235170c6..02c2c1edb7 100644 --- a/samcli/commands/local/lib/swagger/reader.py +++ b/samcli/commands/local/lib/swagger/reader.py @@ -57,7 +57,7 @@ def parse_aws_include_transform(data): return location -class SamSwaggerReader(object): +class SwaggerReader(object): """ Class to read and parse Swagger document from a variety of sources. This class accepts the same data formats as available in Serverless::Api SAM resource diff --git a/samcli/commands/validate/lib/sam_template_validator.py b/samcli/commands/validate/lib/sam_template_validator.py index 773fbf1c0a..0c81c52e50 100644 --- a/samcli/commands/validate/lib/sam_template_validator.py +++ b/samcli/commands/validate/lib/sam_template_validator.py @@ -75,11 +75,18 @@ def _replace_local_codeuri(self): """ all_resources = self.sam_template.get("Resources", {}) + global_settings = self.sam_template.get("Globals", {}) + + for resource_type, properties in global_settings.items(): + + if resource_type == "Function": + + SamTemplateValidator._update_to_s3_uri("CodeUri", properties) for _, resource in all_resources.items(): resource_type = resource.get("Type") - resource_dict = resource.get("Properties") + resource_dict = resource.get("Properties", {}) if resource_type == "AWS::Serverless::Function": @@ -90,7 +97,7 @@ def _replace_local_codeuri(self): SamTemplateValidator._update_to_s3_uri("ContentUri", resource_dict) if resource_type == "AWS::Serverless::Api": - if "DefinitionBody" not in resource_dict: + if "DefinitionUri" in resource_dict: SamTemplateValidator._update_to_s3_uri("DefinitionUri", resource_dict) @staticmethod diff --git a/samcli/lib/intrinsic_resolver/__init__.py b/samcli/lib/intrinsic_resolver/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py new file mode 100644 index 0000000000..3b87f5f23f --- /dev/null +++ b/samcli/lib/intrinsic_resolver/intrinsic_property_resolver.py @@ -0,0 +1,994 @@ +""" +Process and simplifies CloudFormation intrinsic properties such as FN::* and Ref +""" +import copy +import logging + +import base64 +import re +from collections import OrderedDict + +from six import string_types + +from samcli.lib.intrinsic_resolver.invalid_intrinsic_validation import ( + verify_intrinsic_type_list, + verify_non_null, + verify_intrinsic_type_int, + verify_in_bounds, + verify_number_arguments, + verify_intrinsic_type_str, + verify_intrinsic_type_dict, + verify_intrinsic_type_bool, + verify_all_list_intrinsic_type, +) +from samcli.lib.intrinsic_resolver.invalid_intrinsic_exception import ( + InvalidIntrinsicException, + InvalidSymbolException +) + +LOG = logging.getLogger(__name__) + + +class IntrinsicResolver(object): + AWS_INCLUDE = "AWS::Include" + SUPPORTED_MACRO_TRANSFORMATIONS = [AWS_INCLUDE] + _PSEUDO_REGEX = r"AWS::.*?" + _ATTRIBUTE_REGEX = r"[a-zA-Z0-9]*?\.?[a-zA-Z0-9]*?" + _REGEX_SUB_FUNCTION = r"\$\{(" + _PSEUDO_REGEX + "||" + _ATTRIBUTE_REGEX + r")\}" + + FN_JOIN = "Fn::Join" + FN_SPLIT = "Fn::Split" + FN_SUB = "Fn::Sub" + FN_SELECT = "Fn::Select" + FN_BASE64 = "Fn::Base64" + FN_FIND_IN_MAP = "Fn::FindInMap" + FN_TRANSFORM = "Fn::Transform" + FN_GET_AZS = "Fn::GetAZs" + REF = "Ref" + FN_GET_ATT = "Fn::GetAtt" + FN_IMPORT_VALUE = "Fn::ImportValue" + + SUPPORTED_INTRINSIC_FUNCTIONS = [ + FN_JOIN, + FN_SPLIT, + FN_SUB, + FN_SELECT, + FN_BASE64, + FN_FIND_IN_MAP, + FN_TRANSFORM, + FN_GET_AZS, + REF, + FN_GET_ATT, + FN_IMPORT_VALUE, + ] + + FN_AND = "Fn::And" + FN_OR = "Fn::Or" + FN_IF = "Fn::If" + FN_EQUALS = "Fn::Equals" + FN_NOT = "Fn::Not" + + CONDITIONAL_FUNCTIONS = [FN_AND, FN_OR, FN_IF, FN_EQUALS, FN_NOT] + + def __init__(self, template, symbol_resolver): + """ + Initializes the Intrinsic Property class with the default intrinsic_key_function_map and + conditional_key_function_map. + + In the future, for items like Fn::ImportValue multiple templates can be provided + into the function. + """ + self._template = None + self._resources = None + self._mapping = None + self._parameters = None + self._conditions = None + self._outputs = None + self.init_template(template) + + self._symbol_resolver = symbol_resolver + + self.intrinsic_key_function_map = self.default_intrinsic_function_map() + self.conditional_key_function_map = self.default_conditional_key_map() + + def init_template(self, template): + self._template = copy.deepcopy(template or {}) + self._resources = self._template.get("Resources", {}) + self._mapping = self._template.get("Mappings", {}) + self._parameters = self._template.get("Parameters", {}) + self._conditions = self._template.get("Conditions", {}) + self._outputs = self._template.get("Outputs", {}) + + def default_intrinsic_function_map(self): + """ + Returns a dictionary containing the mapping from + Intrinsic Function Key -> Intrinsic Resolver. + The intrinsic_resolver function has the format lambda intrinsic: some_retun_value + + Return + ------- + A dictionary containing the mapping from Intrinsic Function Key -> Intrinsic Resolver + """ + return { + IntrinsicResolver.FN_JOIN: self.handle_fn_join, + IntrinsicResolver.FN_SPLIT: self.handle_fn_split, + IntrinsicResolver.FN_SUB: self.handle_fn_sub, + IntrinsicResolver.FN_SELECT: self.handle_fn_select, + IntrinsicResolver.FN_BASE64: self.handle_fn_base64, + IntrinsicResolver.FN_FIND_IN_MAP: self.handle_find_in_map, + IntrinsicResolver.FN_TRANSFORM: self.handle_fn_transform, + IntrinsicResolver.FN_GET_AZS: self.handle_fn_get_azs, + IntrinsicResolver.REF: self.handle_fn_ref, + IntrinsicResolver.FN_GET_ATT: self.handle_fn_getatt, + IntrinsicResolver.FN_IMPORT_VALUE: self.handle_fn_import_value, + } + + def default_conditional_key_map(self): + """ + Returns a dictionary containing the mapping from Conditional + Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver. + The intrinsic_resolver function has the format lambda intrinsic: some_retun_value + + The code was split between conditionals and other intrinsic keys for readability purposes. + Return + ------- + A dictionary containing the mapping from Intrinsic Function Key -> Intrinsic Resolver + """ + return { + IntrinsicResolver.FN_AND: self.handle_fn_and, + IntrinsicResolver.FN_OR: self.handle_fn_or, + IntrinsicResolver.FN_IF: self.handle_fn_if, + IntrinsicResolver.FN_EQUALS: self.handle_fn_equals, + IntrinsicResolver.FN_NOT: self.handle_fn_not, + } + + def set_intrinsic_key_function_map(self, function_map): + """ + Sets the mapping from + Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver. + The intrinsic_resolver function has the format lambda intrinsic: some_retun_value + + A user of this function can set the function map directly or can get the default_conditional_key_map directly. + + + """ + self.intrinsic_key_function_map = function_map + + def set_conditional_function_map(self, function_map): + """ + Sets the mapping from + Conditional Intrinsic Function Key -> Conditional Intrinsic Resolver. + The intrinsic_resolver function has the format lambda intrinsic: some_retun_value + + A user of this function can set the function map directly or can get the default_intrinsic_function_map directly + + The code was split between conditionals and other intrinsic keys for readability purposes. + + """ + self.conditional_key_function_map = function_map + + def intrinsic_property_resolver(self, intrinsic, parent_function="template"): + """ + This resolves the intrinsic of the format + { + intrinsic: dict + } by calling the function with the relevant intrinsic function resolver. + + This also supports returning a string, list, boolean, int since they may be intermediate steps in the recursion + process. No transformations are done on these. + + By default this will just return the item if non of the types match. This is because of the function + resolve_all_attributes which will recreate the resources by processing every aspect of resource. + + This code resolves in a top down depth first fashion in order to create a functional style recursion that + doesn't mutate any of the properties. + + Parameters + ---------- + intrinsic: dict, str, list, bool, int + This is an intrinsic property or an intermediate step + parent_function: str + In case there is a missing property, this is used to figure out where the property resolved is missing. + Return + --------- + The simplified version of the intrinsic function. This could be a list,str,dict depending on the format required + """ + if intrinsic is None: + raise InvalidIntrinsicException("Missing Intrinsic property in {}".format(parent_function)) + if any(isinstance(intrinsic, object_type) for object_type in [string_types, bool, int]) or intrinsic == {}: + return intrinsic + if isinstance(intrinsic, list): + return [self.intrinsic_property_resolver(item) for item in intrinsic] + + keys = list(intrinsic.keys()) + key = keys[0] + + if key in self.intrinsic_key_function_map: + intrinsic_value = intrinsic.get(key) + return self.intrinsic_key_function_map.get(key)(intrinsic_value) + elif key in self.conditional_key_function_map: + intrinsic_value = intrinsic.get(key) + return self.conditional_key_function_map.get(key)(intrinsic_value) + + # In this case, it is a dictionary that doesn't directly contain an intrinsic resolver, we must recursively + # resolve each of it's sub properties. + sanitized_dict = {} + for key, val in intrinsic.items(): + sanitized_key = self.intrinsic_property_resolver(key, parent_function=parent_function) + sanitized_val = self.intrinsic_property_resolver(val, parent_function=parent_function) + verify_intrinsic_type_str(sanitized_key, + message="The keys of the dictionary {} in {} must all resolve to a string".format( + sanitized_key, parent_function + )) + sanitized_dict[sanitized_key] = sanitized_val + return sanitized_dict + + def resolve_template(self, ignore_errors=False): + """ + This resolves all the attributes of the CloudFormation dictionary Resources, Outputs, Mappings, Parameters, + Conditions. + + Return + ------- + Return a processed template + """ + processed_template = self._template + + if self._resources: + processed_template["Resources"] = self.resolve_attribute(self._resources, ignore_errors) + if self._outputs: + processed_template["Outputs"] = self.resolve_attribute(self._outputs, ignore_errors) + + return processed_template + + def resolve_attribute(self, cloud_formation_property, ignore_errors=False): + """ + This will parse through every entry in a CloudFormation root key and resolve them based on the symbol_resolver. + Customers can optionally ignore resource errors and default to whatever the resource provides. + + Parameters + ----------- + cloud_formation_property: dict + A high Level dictionary containg either the Mappings, Resources, Outputs, or Parameters Dictionary + ignore_errors: bool + An option to ignore errors that are InvalidIntrinsicException and InvalidSymbolException + Return + ------- + A resolved template with all references possible simplified + """ + processed_dict = OrderedDict() + for key, val in cloud_formation_property.items(): + processed_key = self._symbol_resolver.get_translation(key) or key + try: + processed_resource = self.intrinsic_property_resolver(val, parent_function=processed_key) + processed_dict[processed_key] = processed_resource + except (InvalidIntrinsicException, InvalidSymbolException) as e: + resource_type = val.get("Type", "") + if ignore_errors: + LOG.error( + "Unable to process properties of %s.%s", key, resource_type + ) + processed_dict[key] = val + else: + raise InvalidIntrinsicException( + "Exception with property of {}.{}".format(key, resource_type) + ": " + str(e.args) + ) + return processed_dict + + def handle_fn_join(self, intrinsic_value): + """ + { "Fn::Join" : [ "delimiter", [ comma-delimited list of values ] ] } + This function will join the items in the list together based on the string using the python join. + + This intrinsic function will resolve all the objects within the function's value and check their type. + + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Join intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + arguments = self.intrinsic_property_resolver(intrinsic_value, parent_function=IntrinsicResolver.FN_JOIN) + + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_JOIN) + + delimiter = arguments[0] + + verify_intrinsic_type_str(delimiter, IntrinsicResolver.FN_JOIN, position_in_list="first") + + value_list = self.intrinsic_property_resolver(arguments[1], parent_function=IntrinsicResolver.FN_JOIN) + + verify_intrinsic_type_list( + value_list, + IntrinsicResolver.FN_JOIN, + message="The list of values in {} after the " + "delimiter must be a list".format(IntrinsicResolver.FN_JOIN), + ) + + sanitized_value_list = [ + self.intrinsic_property_resolver( + item, parent_function=IntrinsicResolver.FN_JOIN + ) + for item in value_list + ] + verify_all_list_intrinsic_type( + sanitized_value_list, + verification_func=verify_intrinsic_type_str, + property_type=IntrinsicResolver.FN_JOIN, + ) + + return delimiter.join(sanitized_value_list) + + def handle_fn_split(self, intrinsic_value): + """ + { "Fn::Split" : [ "delimiter", "source string" ] } + This function will then split the source_string based on the delimiter + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Split intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_SPLIT + ) + + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_SPLIT) + + delimiter = arguments[0] + + verify_intrinsic_type_str( + delimiter, IntrinsicResolver.FN_SPLIT, position_in_list="first" + ) + + source_string = self.intrinsic_property_resolver( + arguments[1], parent_function=IntrinsicResolver.FN_SPLIT + ) + + verify_intrinsic_type_str( + source_string, IntrinsicResolver.FN_SPLIT, position_in_list="second" + ) + + return source_string.split(delimiter) + + def handle_fn_base64(self, intrinsic_value): + """ + { "Fn::Base64" : valueToEncode } + This intrinsic function will then base64 encode the string using python's base64. + + This function will resolve all the intrinsic properties in valueToEncode + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Base64 intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + data = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_BASE64 + ) + + verify_intrinsic_type_str(data, IntrinsicResolver.FN_BASE64) + # Encoding then decoding is required to return a string of the data + return base64.b64encode(data.encode()).decode() + + def handle_fn_select(self, intrinsic_value): + """ + { "Fn::Select" : [ index, listOfObjects ] } + It will select the item in the listOfObjects using python's base64. + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Select intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_SELECT + ) + + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_SELECT) + + index = self.intrinsic_property_resolver( + arguments[0], parent_function=IntrinsicResolver.FN_SELECT + ) + + verify_intrinsic_type_int(index, IntrinsicResolver.FN_SELECT) + + list_of_objects = self.intrinsic_property_resolver( + arguments[1], parent_function=IntrinsicResolver.FN_SELECT + ) + verify_intrinsic_type_list(list_of_objects, IntrinsicResolver.FN_SELECT) + + sanitized_objects = [ + self.intrinsic_property_resolver( + item, parent_function=IntrinsicResolver.FN_SELECT + ) + for item in list_of_objects + ] + + verify_in_bounds( + index=index, + objects=sanitized_objects, + property_type=IntrinsicResolver.FN_SELECT, + ) + + return sanitized_objects[index] + + def handle_find_in_map(self, intrinsic_value): + """ + { "Fn::FindInMap" : [ "MapName", "TopLevelKey", "SecondLevelKey"] } This function will then lookup the + specified dictionary in the Mappings dictionary as mappings[map_name][top_level_key][second_level_key]. + + This intrinsic function will resolve all the objects within the function's value and check their type. + + The format of the Mappings dictionary is: + "Mappings": { + "map_name": { + "top_level_key": { + "second_level_key": "value" + } + } + } + } + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::FindInMap intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_FIND_IN_MAP + ) + + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_FIND_IN_MAP) + + verify_number_arguments( + arguments, num=3, property_type=IntrinsicResolver.FN_FIND_IN_MAP + ) + + map_name = self.intrinsic_property_resolver( + arguments[0], parent_function=IntrinsicResolver.FN_FIND_IN_MAP + ) + top_level_key = self.intrinsic_property_resolver( + arguments[1], parent_function=IntrinsicResolver.FN_FIND_IN_MAP + ) + second_level_key = self.intrinsic_property_resolver( + arguments[2], parent_function=IntrinsicResolver.FN_FIND_IN_MAP + ) + + verify_intrinsic_type_str( + map_name, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="first" + ) + verify_intrinsic_type_str( + top_level_key, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="second" + ) + verify_intrinsic_type_str( + second_level_key, IntrinsicResolver.FN_FIND_IN_MAP, position_in_list="third" + ) + + map_value = self._mapping.get(map_name) + verify_intrinsic_type_dict( + map_value, + IntrinsicResolver.FN_FIND_IN_MAP, + position_in_list="first", + message="The MapName is missing in the Mappings dictionary in Fn::FindInMap for {}".format( + map_name + ), + ) + + top_level_value = map_value.get(top_level_key) + verify_intrinsic_type_dict( + top_level_value, + IntrinsicResolver.FN_FIND_IN_MAP, + message="The TopLevelKey is missing in the Mappings dictionary in Fn::FindInMap " + "for {}".format(top_level_key), + ) + + second_level_value = top_level_value.get(second_level_key) + verify_intrinsic_type_str( + second_level_value, + IntrinsicResolver.FN_FIND_IN_MAP, + message="The SecondLevelKey is missing in the Mappings dictionary in Fn::FindInMap " + "for {}".format(second_level_key), + ) + + return second_level_value + + def handle_fn_get_azs(self, intrinsic_value): + """ + { "Fn::GetAZs" : "" } + { "Fn::GetAZs" : { "Ref" : "AWS::Region" } } + { "Fn::GetAZs" : "us-east-1" } + This intrinsic function will get the availability zones specified for the specified region. This is usually used + with {"Ref": "AWS::Region"}. If it is an empty string, it will get the default region. + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::GetAZs intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + intrinsic_value = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_GET_AZS + ) + verify_intrinsic_type_str(intrinsic_value, IntrinsicResolver.FN_GET_AZS) + + if intrinsic_value == "": + intrinsic_value = self._symbol_resolver.handle_pseudo_region() + + if intrinsic_value not in self._symbol_resolver.REGIONS: + raise InvalidIntrinsicException( + "Invalid region string passed in to {}".format( + IntrinsicResolver.FN_GET_AZS + ) + ) + + return self._symbol_resolver.REGIONS.get(intrinsic_value) + + def handle_fn_transform(self, intrinsic_value): + """ + { "Fn::Transform" : { "Name" : macro name, "Parameters" : {key : value, ... } } } + This intrinsic function will transform the data with the body provided + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Transform intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + macro_name = intrinsic_value.get("Name") + name = self.intrinsic_property_resolver( + macro_name, parent_function=IntrinsicResolver.FN_TRANSFORM + ) + + if name not in IntrinsicResolver.SUPPORTED_MACRO_TRANSFORMATIONS: + raise InvalidIntrinsicException( + "The type {} is not currently supported in {}".format( + name, IntrinsicResolver.FN_TRANSFORM + ) + ) + + parameters = intrinsic_value.get("Parameters") + verify_intrinsic_type_dict( + parameters, + IntrinsicResolver.FN_TRANSFORM, + message=" Fn::Transform requires parameters section", + ) + + location = self.intrinsic_property_resolver(parameters.get("Location")) + return location + + def handle_fn_import_value(self, intrinsic_value): + """ + { "Fn::ImportValue" : sharedValueToImport } + This intrinsic function requires handling multiple stacks, which is not currently supported by SAM-CLI. + Thus, it will thrown an exception. + + Return + ------- + An InvalidIntrinsicException + """ + raise InvalidIntrinsicException( + "Fn::ImportValue is currently not supported by IntrinsicResolver" + ) + + def handle_fn_getatt(self, intrinsic_value): + """ + { "Fn::GetAtt" : [ "logicalNameOfResource", "attributeName" ] } + This intrinsic function gets the attribute for logical_resource specified. Each attribute might have a different + functionality depending on the type. + + This intrinsic function will resolve all the objects within the function's value and check their type. + This calls the symbol resolver in order to resolve the relevant attribute. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::GetAtt intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_GET_ATT + ) + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_GET_ATT) + verify_number_arguments(arguments, IntrinsicResolver.FN_GET_ATT, num=2) + + logical_id = self.intrinsic_property_resolver( + arguments[0], parent_function=IntrinsicResolver.FN_GET_ATT + ) + resource_type = self.intrinsic_property_resolver( + arguments[1], parent_function=IntrinsicResolver.FN_GET_ATT + ) + + verify_intrinsic_type_str(logical_id, IntrinsicResolver.FN_GET_ATT) + verify_intrinsic_type_str(resource_type, IntrinsicResolver.FN_GET_ATT) + + return self._symbol_resolver.resolve_symbols(logical_id, resource_type) + + def handle_fn_ref(self, intrinsic_value): + """ + {"Ref": "Logical ID"} + This intrinsic function gets the reference to a certain attribute. Some Ref's have different functionality with + different resource types. + + This intrinsic function will resolve all the objects within the function's value and check their type. + This calls the symbol resolver in order to resolve the relevant attribute. + Parameter + ---------- + intrinsic_value: str + This is the value of the object inside the Ref intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.REF + ) + verify_intrinsic_type_str(arguments, IntrinsicResolver.REF) + + return self._symbol_resolver.resolve_symbols(arguments, IntrinsicResolver.REF) + + def handle_fn_sub(self, intrinsic_value): + """ + { "Fn::Sub" : [ String, { Var1Name: Var1Value, Var2Name: Var2Value } ] } or { "Fn::Sub" : String } + This intrinsic function will substitute the variables specified in the list into the string provided. The string + will also parse out pseudo properties and anything of the form ${}. + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Join intrinsic function property + + Return + ------- + A string with the resolved attributes + """ + + def resolve_sub_attribute(intrinsic_item, symbol_resolver): + if "." in intrinsic_item: + (logical_id, attribute_type) = intrinsic_item.rsplit(".", 1) + else: + (logical_id, attribute_type) = intrinsic_item, IntrinsicResolver.REF + return symbol_resolver.resolve_symbols( + logical_id, attribute_type, ignore_errors=True + ) + + if isinstance(intrinsic_value, string_types): + intrinsic_value = [intrinsic_value, {}] + + verify_intrinsic_type_list( + intrinsic_value, + IntrinsicResolver.FN_SUB, + message="The arguments to a Fn::Sub must be a list or a string", + ) + + verify_number_arguments(intrinsic_value, IntrinsicResolver.FN_SUB, num=2) + + sub_str = self.intrinsic_property_resolver( + intrinsic_value[0], parent_function=IntrinsicResolver.FN_SUB + ) + verify_intrinsic_type_str( + sub_str, IntrinsicResolver.FN_SUB, position_in_list="first" + ) + + variables = intrinsic_value[1] + verify_intrinsic_type_dict( + variables, IntrinsicResolver.FN_SUB, position_in_list="second" + ) + + sanitized_variables = self.intrinsic_property_resolver( + variables, parent_function=IntrinsicResolver.FN_SUB + ) + + subable_props = re.findall( + string=sub_str, pattern=IntrinsicResolver._REGEX_SUB_FUNCTION + ) + for sub_item in subable_props: + sanitized_item = ( + sanitized_variables[sub_item] + if sub_item in sanitized_variables + else sub_item + ) + result = resolve_sub_attribute(sanitized_item, self._symbol_resolver) + sub_str = re.sub( + pattern=r"\$\{" + sub_item + r"\}", string=sub_str, repl=result + ) + return sub_str + + def handle_fn_if(self, intrinsic_value): + """ + {"Fn::If": [condition_name, value_if_true, value_if_false]} + This intrinsic function will evaluate the condition from the Conditions dictionary and then return value_if_true + or value_if_false depending on the value. + + The Conditions dictionary will have the following format: + { + "Conditions": { + "condition_name": True/False or "{Intrinsic Function}" + } + } + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Join intrinsic function property + + Return + ------- + This will return value_if_true and value_if_false depending on how the condition is evaluated + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_IF + ) + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_IF) + verify_number_arguments(arguments, IntrinsicResolver.FN_IF, num=3) + + condition_name = self.intrinsic_property_resolver( + arguments[0], parent_function=IntrinsicResolver.FN_IF + ) + verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_IF) + + value_if_true = self.intrinsic_property_resolver( + arguments[1], parent_function=IntrinsicResolver.FN_IF + ) + value_if_false = self.intrinsic_property_resolver( + arguments[2], parent_function=IntrinsicResolver.FN_IF + ) + + condition = self._conditions.get(condition_name) + verify_intrinsic_type_dict( + condition, + IntrinsicResolver.FN_IF, + message="The condition is missing in the Conditions dictionary for {}".format( + IntrinsicResolver.FN_IF + ), + ) + + condition_evaluated = self.intrinsic_property_resolver( + condition, parent_function=IntrinsicResolver.FN_IF + ) + verify_intrinsic_type_bool( + condition_evaluated, + IntrinsicResolver.FN_IF, + message="The result of {} must evaluate to bool".format( + IntrinsicResolver.FN_IF + ), + ) + + return value_if_true if condition_evaluated else value_if_false + + def handle_fn_equals(self, intrinsic_value): + """ + {"Fn::Equals" : ["value_1", "value_2"]} + This intrinsic function will verify that both items in the intrinsic function are equal after resolving them. + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Join intrinsic function property + + Return + ------- + A boolean depending on if both arguments is equal + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_EQUALS + ) + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_EQUALS) + verify_number_arguments(arguments, IntrinsicResolver.FN_EQUALS, num=2) + + value_1 = self.intrinsic_property_resolver( + arguments[0], parent_function=IntrinsicResolver.FN_EQUALS + ) + value_2 = self.intrinsic_property_resolver( + arguments[1], parent_function=IntrinsicResolver.FN_EQUALS + ) + return value_1 == value_2 + + def handle_fn_not(self, intrinsic_value): + """ + {"Fn::Not": [{condition}]} + This intrinsic function will negate the evaluation of the condition specified. + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Join intrinsic function property + + Return + ------- + A boolean that is the opposite of the condition evaluated + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_NOT + ) + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_NOT) + verify_number_arguments(arguments, IntrinsicResolver.FN_NOT, num=1) + argument_sanitised = self.intrinsic_property_resolver( + arguments[0], parent_function=IntrinsicResolver.FN_NOT + ) + if isinstance(argument_sanitised, dict) and "Condition" in arguments[0]: + condition_name = argument_sanitised.get("Condition") + verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_NOT) + + condition = self._conditions.get(condition_name) + verify_non_null( + condition, IntrinsicResolver.FN_NOT, position_in_list="first" + ) + + argument_sanitised = self.intrinsic_property_resolver( + condition, parent_function=IntrinsicResolver.FN_NOT + ) + + verify_intrinsic_type_bool( + argument_sanitised, + IntrinsicResolver.FN_NOT, + message="The result of {} must evaluate to bool".format( + IntrinsicResolver.FN_NOT + ), + ) + return not argument_sanitised + + @staticmethod + def get_prefix_position_in_list(i): + """ + Gets the prefix for the string "ith element of the list", handling first, second, and third. + :param i: + :return: + """ + prefix = "{} th ".format(str(i)) + if i == 1: + prefix = "first " + elif i == 2: + prefix = "second " + elif i == 3: + prefix = "third " + return prefix + + def handle_fn_and(self, intrinsic_value): + """ + {"Fn::And": [{condition}, {...}]} + This intrinsic checks that every item in the list evaluates to a boolean. The items in the list can either + be of the format {Condition: condition_name} which finds and evaluates the Conditions dictionary of another + intrinsic function. + + The Conditions dictionary will have the following format: + { + "Conditions": { + "condition_name": True/False or "{Intrinsic Function}" + } + } + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Join intrinsic function property + + Return + ------- + A boolean depending on if all of the properties in Fn::And evaluate to True + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_AND + ) + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_AND) + + for i, argument in enumerate(arguments): + if isinstance(argument, dict) and "Condition" in argument: + condition_name = argument.get("Condition") + verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_AND) + + condition = self._conditions.get(condition_name) + verify_non_null( + condition, + IntrinsicResolver.FN_AND, + position_in_list=self.get_prefix_position_in_list(i), + ) + + condition_evaluated = self.intrinsic_property_resolver( + condition, parent_function=IntrinsicResolver.FN_AND + ) + verify_intrinsic_type_bool( + condition_evaluated, IntrinsicResolver.FN_AND + ) + + if not condition_evaluated: + return False + else: + condition = self.intrinsic_property_resolver( + argument, parent_function=IntrinsicResolver.FN_AND + ) + verify_intrinsic_type_bool(condition, IntrinsicResolver.FN_AND) + + if not condition: + return False + + return True + + def handle_fn_or(self, intrinsic_value): + """ + {"Fn::Or": [{condition}, {...}]} + This intrinsic checks that a single item in the list evaluates to a boolean. The items in the list can either + be of the format {Condition: condition_name} which finds and evaluates the Conditions dictionary of another + intrinsic function. + + The Conditions dictionary will have the following format: + { + "Conditions": { + "condition_name": True/False or "{Intrinsic Function}" + } + } + + This intrinsic function will resolve all the objects within the function's value and check their type. + Parameter + ---------- + intrinsic_value: list, dict + This is the value of the object inside the Fn::Join intrinsic function property + + Return + ------- + A boolean depending on if any of the properties in Fn::And evaluate to True + """ + arguments = self.intrinsic_property_resolver( + intrinsic_value, parent_function=IntrinsicResolver.FN_OR + ) + verify_intrinsic_type_list(arguments, IntrinsicResolver.FN_OR) + for i, argument in enumerate(arguments): + if isinstance(argument, dict) and "Condition" in argument: + condition_name = argument.get("Condition") + verify_intrinsic_type_str(condition_name, IntrinsicResolver.FN_OR) + + condition = self._conditions.get(condition_name) + verify_non_null( + condition, + IntrinsicResolver.FN_OR, + position_in_list=self.get_prefix_position_in_list(i), + ) + + condition_evaluated = self.intrinsic_property_resolver( + condition, parent_function=IntrinsicResolver.FN_OR + ) + verify_intrinsic_type_bool(condition_evaluated, IntrinsicResolver.FN_OR) + if condition_evaluated: + return True + else: + condition = self.intrinsic_property_resolver( + argument, parent_function=IntrinsicResolver.FN_OR + ) + verify_intrinsic_type_bool(condition, IntrinsicResolver.FN_OR) + if condition: + return True + return False diff --git a/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py b/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py new file mode 100644 index 0000000000..922ed92577 --- /dev/null +++ b/samcli/lib/intrinsic_resolver/intrinsics_symbol_table.py @@ -0,0 +1,441 @@ +""" +The symbol table that is used in IntrinsicResolver in order to resolve runtime attributes +""" +import logging +import os + +from six import string_types + +from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver +from samcli.lib.intrinsic_resolver.invalid_intrinsic_exception import ( + InvalidSymbolException, +) + +LOG = logging.getLogger(__name__) + + +class IntrinsicsSymbolTable(object): + AWS_ACCOUNT_ID = "AWS::AccountId" + AWS_NOTIFICATION_ARN = "AWS::NotificationArn" + AWS_PARTITION = "AWS::Partition" + AWS_REGION = "AWS::Region" + AWS_STACK_ID = "AWS::StackId" + AWS_STACK_NAME = "AWS::StackName" + AWS_URL_PREFIX = "AWS::URLSuffix" + AWS_NOVALUE = "AWS::NoValue" + SUPPORTED_PSEUDO_TYPES = [ + AWS_ACCOUNT_ID, + AWS_NOTIFICATION_ARN, + AWS_PARTITION, + AWS_REGION, + AWS_STACK_ID, + AWS_STACK_NAME, + AWS_URL_PREFIX, + AWS_NOVALUE, + ] + + # There is not much benefit in infering real values for these parameters in local development context. These values + # are usually representative of an AWS environment and stack, but in local development scenario they don't make + # sense. If customers choose to, they can always override this value through the CLI interface. + DEFAULT_PSEUDO_PARAM_VALUES = { + "AWS::AccountId": "123456789012", + "AWS::Partition": "aws", + "AWS::Region": "us-east-1", + "AWS::StackName": "local", + "AWS::StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/" + "local/51af3dc0-da77-11e4-872e-1234567db123", + "AWS::URLSuffix": "localhost", + } + + REGIONS = { + "us-east-1": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f", + ], + "us-west-1": ["us-west-1b", "us-west-1c"], + "eu-north-1": ["eu-north-1a", "eu-north-1b", "eu-north-1c"], + "ap-northeast-3": ["ap-northeast-3a"], + "ap-northeast-2": ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"], + "ap-northeast-1": ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"], + "sa-east-1": ["sa-east-1a", "sa-east-1c"], + "ap-southeast-1": ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"], + "ca-central-1": ["ca-central-1a", "ca-central-1b"], + "ap-southeast-2": ["ap-southeast-2a", "ap-southeast-2b", "ap-southeast-2c"], + "us-west-2": ["us-west-2a", "us-west-2b", "us-west-2c", "us-west-2d"], + "us-east-2": ["us-east-2a", "us-east-2b", "us-east-2c"], + "ap-south-1": ["ap-south-1a", "ap-south-1b", "ap-south-1c"], + "eu-central-1": ["eu-central-1a", "eu-central-1b", "eu-central-1c"], + "eu-west-1": ["eu-west-1a", "eu-west-1b", "eu-west-1c"], + "eu-west-2": ["eu-west-2a", "eu-west-2b", "eu-west-2c"], + "eu-west-3": ["eu-west-3a", "eu-west-3b", "eu-west-3c"], + "cn-north-1": [], + "us-gov-west-1": [], + } + + DEFAULT_PARTITION = "aws" + GOV_PARTITION = "aws-us-gov" + CHINA_PARTITION = "aws-cn" + CHINA_PREFIX = "cn" + GOV_PREFIX = "gov" + + CHINA_URL_PREFIX = "amazonaws.com.cn" + DEFAULT_URL_PREFIX = "amazonaws.com" + + AWS_NOTIFICATION_SERVICE_NAME = "sns" + ARN_SUFFIX = ".Arn" + + CFN_RESOURCE_TYPE = "Type" + + def __init__( + self, + template=None, + logical_id_translator=None, + default_type_resolver=None, + common_attribute_resolver=None, + ): + """ + Initializes the Intrinsic Symbol Table so that runtime attributes can be resolved. + + The code is defaulted in the following order logical_id_translator => parameters => default_type_resolver => + common_attribute_resolver + + If the item is a pseudo type, it will run through the logical_id_translator and if it doesn't exist there + it will generate a default one and save it in the logical_id_translator as a cache for future computation. + Parameters + ------------ + logical_id_translator: dict + This will act as the default symbol table resolver. The resolver will first check if the attribute is + explicitly defined in this dictionary and do the relevant translation. + + All Logical Ids and Pseudo types can be included here. + { + "RestApi.Test": { # this could be used with RestApi.Deployment => NewRestApi + "Ref": "NewRestApi" + }, + "LambdaFunction": { + "Ref": "LambdaFunction", + "Arn": "MyArn" + } + "AWS::Region": "us-east-1" + } + default_type_resolver: dict + This can be used provide common attributes that are true across all objects of a certain type. + This can be in the format of + { + "AWS::ApiGateway::RestApi": { + "RootResourceId": "/" + } + } + or can also be a function that takes in (logical_id, attribute_type) => string + { + "AWS::ApiGateway::RestApi": { + "RootResourceId": (lambda l, a, p, r: p.get("ResourceId")) + } + } + common_attribute_resolver: dict + This is a clean way of specifying common attributes across all types. + The value can either be a function of the form string or (logical_id) => string + { + "Ref": lambda p,r: "", + "Arn:": arn_resolver + } + """ + self.logical_id_translator = logical_id_translator or {} + + self._template = template or {} + self._parameters = self._template.get("Parameters", {}) + self._resources = self._template.get("Resources", {}) + + self.default_type_resolver = ( + default_type_resolver or self.get_default_type_resolver() + ) + self.common_attribute_resolver = ( + common_attribute_resolver or self.get_default_attribute_resolver() + ) + self.default_pseudo_resolver = self.get_default_pseudo_resolver() + + def get_default_pseudo_resolver(self): + return { + IntrinsicsSymbolTable.AWS_ACCOUNT_ID: self.handle_pseudo_account_id, + IntrinsicsSymbolTable.AWS_PARTITION: self.handle_pseudo_partition, + IntrinsicsSymbolTable.AWS_REGION: self.handle_pseudo_region, + IntrinsicsSymbolTable.AWS_STACK_ID: self.handle_pseudo_stack_id, + IntrinsicsSymbolTable.AWS_STACK_NAME: self.handle_pseudo_stack_name, + IntrinsicsSymbolTable.AWS_NOVALUE: self.handle_pseudo_no_value, + IntrinsicsSymbolTable.AWS_URL_PREFIX: self.handle_pseudo_url_prefix, + } + + def get_default_attribute_resolver(self): + return {"Ref": lambda logical_id: logical_id, "Arn": self.arn_resolver} + + @staticmethod + def get_default_type_resolver(): + return { + "AWS::ApiGateway::RestApi": { + "RootResourceId": "/" # It usually used as a reference to the parent id of the RestApi, + }, + "AWS::Lambda::LayerVersion": { + IntrinsicResolver.REF: lambda logical_id: {IntrinsicResolver.REF: logical_id} + }, + "AWS::Serverless::LayerVersion": { + IntrinsicResolver.REF: lambda logical_id: {IntrinsicResolver.REF: logical_id} + } + } + + def resolve_symbols(self, logical_id, resource_attribute, ignore_errors=False): + """ + This function resolves all the symbols given a logical id and a resource_attribute for Fn::GetAtt and Ref. + This boils Ref into a type of Fn:GetAtt to simplify the implementation. + For example: + {"Ref": "AWS::REGION"} => resolve_symbols("AWS::REGION", "REF") + {"Fn::GetAtt": ["logical_id", "attribute_type"] => resolve_symbols(logical_id, attribute_type) + + + First pseudo types are checked. If item is present in the logical_id_translator it is returned. + Otherwise, it falls back to the default_pseudo_resolver + + Then the default_type_resolver is checked, which has common attributes and functions for each types. + Then the common_attribute_resolver is run, which has functions that are common for each attribute. + Parameters + ----------- + logical_id: str + The logical id of the resource in question or a pseudo type. + resource_attribute: str + The resource attribute of the resource in question or Ref for psuedo types. + ignore_errors: bool + An optional flags to not return errors. This used in sub + + Return + ------- + This resolves the attribute + """ + # pylint: disable-msg=too-many-return-statements + translated = self.get_translation(logical_id, resource_attribute) + if translated: + return translated + + if logical_id in self.SUPPORTED_PSEUDO_TYPES: + translated = self.default_pseudo_resolver.get(logical_id)() + self.logical_id_translator[logical_id] = translated + return translated + + # Handle Default Parameters + translated = self._parameters.get(logical_id, {}).get("Default") + if translated: + return translated + # Handle Default Property Type Resolution + resource_type = self._resources.get(logical_id, {}).get( + IntrinsicsSymbolTable.CFN_RESOURCE_TYPE + ) + + resolver = ( + self.default_type_resolver.get(resource_type, {}).get(resource_attribute) + if resource_type + else {} + ) + if resolver: + if callable(resolver): + return resolver(logical_id) + return resolver + + # Handle Attribute Type Resolution + attribute_resolver = self.common_attribute_resolver.get(resource_attribute, {}) + if attribute_resolver: + if callable(attribute_resolver): + return attribute_resolver(logical_id) + return attribute_resolver + + if ignore_errors: + return "${}".format(logical_id + "." + resource_attribute) + raise InvalidSymbolException( + "The {} is not supported in the logical_id_translator, default_type_resolver, or the attribute_resolver." + " It is also not a supported pseudo function".format( + logical_id + "." + resource_attribute + ) + ) + + def arn_resolver(self, logical_id, service_name="lambda"): + """ + This function resolves Arn in the format + arn:{partition_name}:{service_name}:{aws_region}:{account_id}:{function_name} + + Parameters + ----------- + logical_id: str + This the reference to the function name used + service_name: str + This is the service name used such as lambda or sns + + Return + ------- + The resolved Arn + """ + aws_region = self.handle_pseudo_region() + account_id = ( + self.logical_id_translator.get(IntrinsicsSymbolTable.AWS_ACCOUNT_ID) or self.handle_pseudo_account_id() + ) + partition_name = self.handle_pseudo_partition() + resource_name = logical_id + resource_name = self.logical_id_translator.get(resource_name) or resource_name + str_format = "arn:{partition_name}:{service_name}:{aws_region}:{account_id}:{resource_name}" + if service_name == "lambda": + str_format = "arn:{partition_name}:{service_name}:{aws_region}:{account_id}:function:{resource_name}" + + return str_format.format( + partition_name=partition_name, + service_name=service_name, + aws_region=aws_region, + account_id=account_id, + resource_name=resource_name, + ) + + def get_translation(self, logical_id, resource_attributes=IntrinsicResolver.REF): + """ + This gets the logical_id_translation of the logical id and resource_attributes. + + Parameters + ---------- + logical_id: str + This is the logical id of the resource in question + resource_attributes: str + This is the attribute required. By default, it is a REF type + + Returns + -------- + This returns the translated item if it already exists + + """ + logical_id_item = self.logical_id_translator.get(logical_id, {}) + if any( + isinstance(logical_id_item, object_type) + for object_type in [string_types, list, bool, int] + ): + if ( + resource_attributes != IntrinsicResolver.REF and resource_attributes != "" + ): + return None + return logical_id_item + + return logical_id_item.get(resource_attributes) + + @staticmethod + def get_availability_zone(region): + """ + This gets the availability zone from the the specified region + + Parameters + ----------- + region: str + The specified region from the SymbolTable region + + Return + ------- + The list of availability zones for the specified region + """ + return IntrinsicsSymbolTable.REGIONS.get(region) + + @staticmethod + def handle_pseudo_account_id(): + """ + This gets a default account id from SamBaseProvider. + Return + ------- + A pseudo account id + """ + return IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES.get( + IntrinsicsSymbolTable.AWS_ACCOUNT_ID + ) + + def handle_pseudo_region(self): + """ + Gets the region from the environment and defaults to a the default region from the global variables. + + This is only run if it is not specified by the logical_id_translator as a default. + + Return + ------- + The region from the environment or a default one + """ + return ( + self.logical_id_translator.get(IntrinsicsSymbolTable.AWS_REGION) or os.getenv("AWS_REGION") or + IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES.get( + IntrinsicsSymbolTable.AWS_REGION + ) + ) + + def handle_pseudo_url_prefix(self): + """ + This gets the AWS::UrlSuffix for the intrinsic with the china and regular prefix. + + This is only run if it is not specified by the logical_id_translator as a default. + Return + ------- + The url prefix of amazonaws.com or amazonaws.com.cn + """ + aws_region = self.handle_pseudo_region() + if self.CHINA_PREFIX in aws_region: + return self.CHINA_URL_PREFIX + return self.DEFAULT_URL_PREFIX + + def handle_pseudo_partition(self): + """ + This resolves AWS::Partition so that the correct partition is returned depending on the region. + + This is only run if it is not specified by the logical_id_translator as a default. + + Return + ------- + A pseudo partition like aws-cn or aws or aws-gov + """ + aws_region = self.handle_pseudo_region() + if self.CHINA_PREFIX in aws_region: + return self.CHINA_PARTITION + if self.GOV_PREFIX in aws_region: + return self.GOV_PARTITION + return self.DEFAULT_PARTITION + + @staticmethod + def handle_pseudo_stack_id(): + """ + This resolves AWS::StackId by using the SamBaseProvider as the default value. + + This is only run if it is not specified by the logical_id_translator as a default. + + Return + ------- + A randomized string + """ + return IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES.get( + IntrinsicsSymbolTable.AWS_STACK_ID + ) + + @staticmethod + def handle_pseudo_stack_name(): + """ + This resolves AWS::StackName by using the SamBaseProvider as the default value. + + This is only run if it is not specified by the logical_id_translator as a default. + + Return + ------- + A randomized string + """ + return IntrinsicsSymbolTable.DEFAULT_PSEUDO_PARAM_VALUES.get( + IntrinsicsSymbolTable.AWS_STACK_NAME + ) + + @staticmethod + def handle_pseudo_no_value(): + """ + This resolves AWS::NoValue so that it returns the python None + + Returns + -------- + None + :return: + """ + return None diff --git a/samcli/lib/intrinsic_resolver/invalid_intrinsic_exception.py b/samcli/lib/intrinsic_resolver/invalid_intrinsic_exception.py new file mode 100644 index 0000000000..23bad5899d --- /dev/null +++ b/samcli/lib/intrinsic_resolver/invalid_intrinsic_exception.py @@ -0,0 +1,11 @@ +""" +A custom Exception to display Invalid Intrinsics and Symbol Table format. +""" + + +class InvalidIntrinsicException(Exception): + pass + + +class InvalidSymbolException(Exception): + pass diff --git a/samcli/lib/intrinsic_resolver/invalid_intrinsic_validation.py b/samcli/lib/intrinsic_resolver/invalid_intrinsic_validation.py new file mode 100644 index 0000000000..9c82ced267 --- /dev/null +++ b/samcli/lib/intrinsic_resolver/invalid_intrinsic_validation.py @@ -0,0 +1,103 @@ +""" +A list of helper functions that cleanup the processing in IntrinsicResolver and IntrinsicSymbolTable +""" +from six import string_types + +from samcli.lib.intrinsic_resolver.invalid_intrinsic_exception import InvalidIntrinsicException + + +def verify_intrinsic_type_bool( + argument, property_type="", message="", position_in_list="" +): + verify_intrinsic_type( + argument, property_type, message, position_in_list, primitive_type=bool + ) + + +def verify_intrinsic_type_list( + argument, property_type="", message="", position_in_list="" +): + verify_intrinsic_type( + argument, property_type, message, position_in_list, primitive_type=list + ) + + +def verify_intrinsic_type_dict( + argument, property_type="", message="", position_in_list="" +): + verify_intrinsic_type( + argument, property_type, message, position_in_list, primitive_type=dict + ) + + +def verify_intrinsic_type_int( + argument, property_type="", message="", position_in_list="" +): + # Special case since bool is a subclass of int in python + if isinstance(argument, bool): + raise InvalidIntrinsicException( + message or "The {} argument to {} must resolve to a {} type".format( + position_in_list, property_type, int + ) + ) + verify_intrinsic_type( + argument, property_type, message, position_in_list, primitive_type=int + ) + + +def verify_intrinsic_type_str( + argument, property_type="", message="", position_in_list="" +): + verify_intrinsic_type( + argument, property_type, message, position_in_list, primitive_type=string_types + ) + + +def verify_non_null(argument, property_type="", message="", position_in_list=""): + if argument is None: + raise InvalidIntrinsicException( + message or "The {} argument to {} is missing from the intrinsic function".format( + position_in_list, property_type + ) + ) + + +def verify_intrinsic_type( + argument, + property_type="", + message="", + position_in_list="", + primitive_type=string_types, +): + verify_non_null(argument, property_type, message, position_in_list) + if not isinstance(argument, primitive_type): + raise InvalidIntrinsicException( + message or "The {} argument to {} must resolve to a {} type".format( + position_in_list, property_type, primitive_type + ) + ) + + +def verify_in_bounds(objects, index, property_type=""): + if index < 0 or index >= len(objects): + raise InvalidIntrinsicException( + "The index of {} resolved properties must be within the range".format( + property_type + ) + ) + + +def verify_number_arguments(arguments, property_type="", num=0): + if not len(arguments) == num: + raise InvalidIntrinsicException( + "The arguments to {} must have {} arguments instead of {} arguments".format( + property_type, num, len(arguments) + ) + ) + + +def verify_all_list_intrinsic_type( + arguments, verification_func, property_type="", message="", position_in_list="" +): + for argument in arguments: + verification_func(argument, property_type, message, position_in_list) diff --git a/samcli/lib/samlib/cloudformation_command.py b/samcli/lib/samlib/cloudformation_command.py index c284e05e00..83cd155004 100644 --- a/samcli/lib/samlib/cloudformation_command.py +++ b/samcli/lib/samlib/cloudformation_command.py @@ -56,4 +56,4 @@ def find_executable(execname): except OSError as ex: LOG.debug("Unable to find executable %s", name, exc_info=ex) - raise OSError("Unable to find AWS CLI installation under following names: {}".format(options)) + raise OSError("Cannot find AWS CLI installation, was looking at executables with names: {}".format(options)) diff --git a/samcli/lib/utils/sam_logging.py b/samcli/lib/utils/sam_logging.py new file mode 100644 index 0000000000..47274c479c --- /dev/null +++ b/samcli/lib/utils/sam_logging.py @@ -0,0 +1,31 @@ +""" +Configures a logger +""" +import logging + + +class SamCliLogger(object): + + @staticmethod + def configure_logger(logger, formatter, level): + """ + Configure a Logger with the formatter provided. + + Parameters + ---------- + logger logging.getLogger + Logger to configure + formatter logging.formatter + Formatter for the logger + + Returns + ------- + None + """ + log_stream_handler = logging.StreamHandler() + log_stream_handler.setLevel(logging.DEBUG) + log_stream_handler.setFormatter(formatter) + + logger.setLevel(level) + logger.propagate = False + logger.addHandler(log_stream_handler) diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index 7aef82dc06..37425e2520 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -7,6 +7,7 @@ from flask import Flask, request from werkzeug.datastructures import Headers +from samcli.commands.local.lib.provider import Cors from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser from samcli.lib.utils.stream_writer import StreamWriter from samcli.local.lambdafn.exceptions import FunctionNotFound @@ -18,35 +19,63 @@ class Route(object): - - def __init__(self, methods, function_name, path, binary_types=None, stage_name=None, stage_variables=None): + ANY_HTTP_METHODS = ["GET", + "DELETE", + "PUT", + "POST", + "HEAD", + "OPTIONS", + "PATCH"] + + def __init__(self, function_name, path, methods): """ Creates an ApiGatewayRoute - :param list(str) methods: List of HTTP Methods + :param list(str) methods: http method :param function_name: Name of the Lambda function this API is connected to :param str path: Path off the base url """ - self.methods = methods + self.methods = self.normalize_method(methods) self.function_name = function_name self.path = path - self.binary_types = binary_types or [] - self.stage_name = stage_name - self.stage_variables = stage_variables + + def __eq__(self, other): + return isinstance(other, Route) and \ + sorted(self.methods) == sorted( + other.methods) and self.function_name == other.function_name and self.path == other.path + + def __hash__(self): + route_hash = hash(self.function_name) * hash(self.path) + for method in sorted(self.methods): + route_hash *= hash(method) + return route_hash + + def normalize_method(self, methods): + """ + Normalizes Http Methods. Api Gateway allows a Http Methods of ANY. This is a special verb to denote all + supported Http Methods on Api Gateway. + + :param list methods: Http methods + :return list: Either the input http_method or one of the _ANY_HTTP_METHODS (normalized Http Methods) + """ + methods = [method.upper() for method in methods] + if "ANY" in methods: + return self.ANY_HTTP_METHODS + return methods class LocalApigwService(BaseLocalService): _DEFAULT_PORT = 3000 _DEFAULT_HOST = '127.0.0.1' - def __init__(self, routing_list, lambda_runner, static_dir=None, port=None, host=None, stderr=None): + def __init__(self, api, lambda_runner, static_dir=None, port=None, host=None, stderr=None): """ Creates an ApiGatewayService Parameters ---------- - routing_list list(ApiGatewayCallModel) - A list of the Model that represent the service paths to create. + api: Api + an Api object that contains the list of routes and properties lambda_runner samcli.commands.local.lib.local_lambda.LocalLambdaRunner The Lambda runner class capable of invoking the function static_dir str @@ -61,7 +90,7 @@ def __init__(self, routing_list, lambda_runner, static_dir=None, port=None, host Optional stream writer where the stderr from Docker container should be written to """ super(LocalApigwService, self).__init__(lambda_runner.is_debugging(), port=port, host=host) - self.routing_list = routing_list + self.api = api self.lambda_runner = lambda_runner self.static_dir = static_dir self._dict_of_routes = {} @@ -77,12 +106,11 @@ def create(self): static_folder=self.static_dir # Serve static files from this directory ) - for api_gateway_route in self.routing_list: + for api_gateway_route in self.api.routes: path = PathConverter.convert_path_to_flask(api_gateway_route.path) for route_key in self._generate_route_keys(api_gateway_route.methods, path): self._dict_of_routes[route_key] = api_gateway_route - self._app.add_url_rule(path, endpoint=path, view_func=self._request_handler, @@ -141,11 +169,18 @@ def _request_handler(self, **kwargs): ------- Response object """ + route = self._get_current_route(request) + cors_headers = Cors.cors_to_headers(self.api.cors) + + method, _ = self.get_request_methods_endpoints(request) + if method == 'OPTIONS': + headers = Headers(cors_headers) + return self.service_response('', headers, 200) try: - event = self._construct_event(request, self.port, route.binary_types, route.stage_name, - route.stage_variables) + event = self._construct_event(request, self.port, self.api.binary_media_types, self.api.stage_name, + self.api.stage_variables) except UnicodeDecodeError: return ServiceErrorResponses.lambda_failure_response() @@ -165,7 +200,7 @@ def _request_handler(self, **kwargs): try: (status_code, headers, body) = self._parse_lambda_output(lambda_response, - route.binary_types, + self.api.binary_media_types, request) except (KeyError, TypeError, ValueError): LOG.error("Function returned an invalid response (must include one of: body, headers, multiValueHeaders or " @@ -181,8 +216,7 @@ def _get_current_route(self, flask_request): :param request flask_request: Flask Request :return: Route matching the endpoint and method of the request """ - endpoint = flask_request.endpoint - method = flask_request.method + method, endpoint = self.get_request_methods_endpoints(flask_request) route_key = self._route_key(method, endpoint) route = self._dict_of_routes.get(route_key, None) @@ -195,6 +229,16 @@ def _get_current_route(self, flask_request): return route + def get_request_methods_endpoints(self, flask_request): + """ + Separated out for testing requests in request handler + :param request flask_request: Flask Request + :return: the request's endpoint and method + """ + endpoint = flask_request.endpoint + method = flask_request.method + return method, endpoint + # Consider moving this out to its own class. Logic is started to get dense and looks messy @jfuss @staticmethod def _parse_lambda_output(lambda_output, binary_types, flask_request): @@ -204,6 +248,7 @@ def _parse_lambda_output(lambda_output, binary_types, flask_request): :param str lambda_output: Output from Lambda Invoke :return: Tuple(int, dict, str, bool) """ + # pylint: disable-msg=too-many-statements json_output = json.loads(lambda_output) if not isinstance(json_output, dict): @@ -224,6 +269,14 @@ def _parse_lambda_output(lambda_output, binary_types, flask_request): LOG.error(message) raise TypeError(message) + try: + if body: + body = str(body) + except ValueError: + message = "Non null response bodies should be able to convert to string: {}".format(body) + LOG.error(message) + raise TypeError(message) + # API Gateway only accepts statusCode, body, headers, and isBase64Encoded in # a response shape. invalid_keys = LocalApigwService._invalid_apig_response_keys(json_output) @@ -414,6 +467,8 @@ def _event_headers(flask_request, port): Request from Flask int port Forwarded Port + cors_headers dict + Dict of the Cors properties Returns dict (str: str), dict (str: list of str) ------- @@ -434,7 +489,6 @@ def _event_headers(flask_request, port): headers_dict["X-Forwarded-Port"] = str(port) multi_value_headers_dict["X-Forwarded-Port"] = [str(port)] - return headers_dict, multi_value_headers_dict @staticmethod diff --git a/samcli/local/common/runtime_template.py b/samcli/local/common/runtime_template.py index ef67fe93ea..3535553160 100644 --- a/samcli/local/common/runtime_template.py +++ b/samcli/local/common/runtime_template.py @@ -13,10 +13,12 @@ _init_path = str(pathlib.Path(os.path.dirname(__file__)).parent) _templates = os.path.join(_init_path, 'init', 'templates') + +# Note(TheSriram): The ordering of the runtimes list per language is based on the latest to oldest. RUNTIME_DEP_TEMPLATE_MAPPING = { "python": [ { - "runtimes": ["python2.7", "python3.6", "python3.7"], + "runtimes": ["python3.7", "python3.6", "python2.7"], "dependency_manager": "pip", "init_location": os.path.join(_templates, "cookiecutter-aws-sam-hello-python"), "build": True @@ -32,7 +34,7 @@ ], "nodejs": [ { - "runtimes": ["nodejs8.10", "nodejs10.x"], + "runtimes": ["nodejs10.x", "nodejs8.10"], "dependency_manager": "npm", "init_location": os.path.join(_templates, "cookiecutter-aws-sam-hello-nodejs"), "build": True @@ -46,7 +48,7 @@ ], "dotnet": [ { - "runtimes": ["dotnetcore", "dotnetcore1.0", "dotnetcore2.0", "dotnetcore2.1"], + "runtimes": ["dotnetcore2.1", "dotnetcore2.0", "dotnetcore1.0", "dotnetcore"], "dependency_manager": "cli-package", "init_location": os.path.join(_templates, "cookiecutter-aws-sam-hello-dotnet"), "build": True @@ -81,3 +83,6 @@ RUNTIMES = set(itertools.chain(*[c['runtimes'] for c in list( itertools.chain(*(RUNTIME_DEP_TEMPLATE_MAPPING.values())))])) INIT_RUNTIMES = RUNTIMES.union(RUNTIME_DEP_TEMPLATE_MAPPING.keys()) + +# NOTE(TheSriram): Default Runtime Choice when runtime is not chosen +DEFAULT_RUNTIME = RUNTIME_DEP_TEMPLATE_MAPPING['nodejs'][0]['runtimes'][0] diff --git a/samcli/local/docker/lambda_build_container.py b/samcli/local/docker/lambda_build_container.py index b1e3d23077..8952328182 100644 --- a/samcli/local/docker/lambda_build_container.py +++ b/samcli/local/docker/lambda_build_container.py @@ -22,7 +22,7 @@ class LambdaBuildContainer(Container): and if the build was successful, copies back artifacts to the host filesystem """ - _IMAGE_REPO_NAME = "lambci/lambda" + _LAMBCI_IMAGE_REPO_NAME = "lambci/lambda" _BUILDERS_EXECUTABLE = "lambda-builders" def __init__(self, # pylint: disable=too-many-locals @@ -229,4 +229,7 @@ def _convert_to_container_dirs(host_paths_to_convert, host_to_container_path_map @staticmethod def _get_image(runtime): - return "{}:build-{}".format(LambdaBuildContainer._IMAGE_REPO_NAME, runtime) + runtime_to_images = {"nodejs10.x": "amazon/lambda-build-node10.x"} + + return runtime_to_images.get(runtime, + "{}:build-{}".format(LambdaBuildContainer._LAMBCI_IMAGE_REPO_NAME, runtime)) diff --git a/samcli/local/init/__init__.py b/samcli/local/init/__init__.py index 89e4bc52c9..84ec28c6b9 100644 --- a/samcli/local/init/__init__.py +++ b/samcli/local/init/__init__.py @@ -14,7 +14,7 @@ def generate_project( - location=None, runtime="nodejs", dependency_manager=None, + location=None, runtime="nodejs10.x", dependency_manager=None, output_dir=".", name='sam-sample-app', no_input=False): """Generates project using cookiecutter and options given @@ -51,11 +51,9 @@ def generate_project( for mapping in list(itertools.chain(*(RUNTIME_DEP_TEMPLATE_MAPPING.values()))): if runtime in mapping['runtimes'] or any([r.startswith(runtime) for r in mapping['runtimes']]): - if not dependency_manager: + if not dependency_manager or dependency_manager == mapping['dependency_manager']: template = mapping['init_location'] break - elif dependency_manager == mapping['dependency_manager']: - template = mapping['init_location'] if not template: msg = "Lambda Runtime {} does not support dependency manager: {}".format(runtime, dependency_manager) 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 b178a16dbc..17d9cf30ec 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 @@ -1,144 +1,137 @@ # {{ cookiecutter.project_name }} -This is a sample template for {{ cookiecutter.project_name }} +This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. -## Requirements +- src - Code for the application's Lambda function. +- events - Invocation events that you can use to invoke the function. +- test - Unit tests for the application code. +- template.yaml - A template that defines the application's AWS resources. -* AWS CLI already configured with Administrator permission -* [Docker installed](https://www.docker.com/community-edition) -* [SAM CLI installed](https://github.com/awslabs/aws-sam-cli) -* [.NET Core installed](https://www.microsoft.com/net/download) +The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. -Please see the [currently supported patch of each major version of .NET Core](https://github.com/aws/aws-lambda-dotnet#version-status) to ensure your functions are compatible with the AWS Lambda runtime. +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. +The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. -## Recommended Tools for Visual Studio / Visual Studio Code Users +* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) -* [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. +## Deploy the sample application -> **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.** +The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. -## Setup process +To use the SAM CLI, you need the following tools. -### Folder Structure +* AWS CLI - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) and [configure it with your AWS credentials]. +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* .NET Core - [Install .NET Core](https://www.microsoft.com/net/download) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) -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. +The SAM CLI uses an Amazon S3 bucket to store your application's deployment artifacts. If you don't have a bucket suitable for this purpose, create one. Replace `BUCKET_NAME` in the commands in this section with a unique bucket name. -```yaml -... - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: ./src/HelloWorld - ... +```bash +{{ cookiecutter.project_name }}$ aws s3 mb s3://BUCKET_NAME ``` -### Building your application +To prepare the application for deployment, use the `sam package` command. ```bash -sam build +{{ cookiecutter.project_name }}$ sam package \ + --output-template-file packaged.yaml \ + --s3-bucket BUCKET_NAME ``` -### Local development +The SAM CLI creates deployment packages, uploads them to the S3 bucket, and creates a new version of the template that refers to the artifacts in the bucket. -**Invoking function locally** +To deploy the application, use the `sam deploy` command. ```bash -sam local invoke --no-event +{{ cookiecutter.project_name }}$ sam deploy \ + --template-file packaged.yaml \ + --stack-name {{ cookiecutter.project_name }} \ + --capabilities CAPABILITY_IAM ``` -To invoke with an event you can pass in a json file to the command. +After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: ```bash -sam local invoke -e event.json -``` +{{ cookiecutter.project_name }}$ aws cloudformation describe-stacks \ + --stack-name {{ cookiecutter.project_name }} \ + --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ + --output table +``` +## Use the SAM CLI to build and test locally -**Invoking function locally through local API Gateway** +Build your application with the `sam build` command. ```bash -sam local start-api +{{ cookiecutter.project_name }}$ sam build ``` -**SAM Local** is used to emulate both Lambda and API Gateway locally and uses our `template.yaml` to understand how to bootstrap this environment (runtime, where the source code is, etc.) - The following excerpt is what the CLI will read in order to initialize an API and its routes: +The SAM CLI installs dependencies defined in `src/HelloWorld.csproj`, creates a deployment package, and saves it in the `.aws-sam/build` folder. -```yaml -... -Events: - HelloWorldFunction: - 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 -``` - -If the previous command run successfully you should now be able to hit the following local endpoint to invoke your function `http://localhost:3000/hello` +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. -## Packaging and deployment - -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: +Run functions locally and invoke them with the `sam local invoke` command. ```bash -aws s3 mb s3://BUCKET_NAME +{{ cookiecutter.project_name }}$ sam local invoke HelloWorldFunction --event events/event.json ``` -Next, run the following command to package our Lambda function to S3: +The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. ```bash -sam package \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME +{{ cookiecutter.project_name }}$ sam local start-api +{{ cookiecutter.project_name }}$ curl http://localhost:3000/ ``` -Next, the following command will create a Cloudformation Stack and deploy your SAM resources. +The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. -```bash -sam deploy \ - --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --capabilities CAPABILITY_IAM +```yaml + Events: + HelloWorld: + Type: Api + Properties: + Path: /hello + Method: get ``` -> **See [Serverless Application Model (SAM) HOWTO Guide](https://github.com/awslabs/serverless-application-model/blob/master/HOWTO.md) for more details in how to get started.** +## Add a resource to your application +The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. -After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: +## Fetch, tail, and filter Lambda function logs -```bash -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --query 'Stacks[].Outputs' -``` +To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. -## Testing - -For testing our code, we use XUnit and you can use `dotnet test` to run tests defined under `test/` +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. ```bash -dotnet test test/HelloWorld.Test +{{ cookiecutter.project_name }}$ sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name }} --tail ``` -# 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. +You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). -``` bash - sam init --location gh:aws-samples/cookiecutter-aws-sam-dotnet -``` +## Unit tests -For more information and examples of how to use `sam init` run +Tests are defined in the `test` folder in this project. -``` bash -sam init --help +```bash +{{ cookiecutter.project_name }}$ dotnet test test/HelloWorld.Test ``` -## Bringing to the next level +## Cleanup -Here are a few ideas that you can use to get more acquainted as to how this overall process works: +To delete the sample application and the bucket that you created, use the AWS CLI. + +```bash +{{ cookiecutter.project_name }}$ aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name }} +{{ cookiecutter.project_name }}$ aws s3 rb s3://BUCKET_NAME +``` -* Create an additional API resource (e.g. /hello/{proxy+}) and return the name requested through this new path -* Update unit test to capture that -* Package & Deploy +## Resources -Next, you can use the following resources to know more about beyond hello world samples and how others structure their Serverless applications: +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. -* [AWS Serverless Application Repository](https://aws.amazon.com/serverless/serverlessrepo/) +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/events/event.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/events/event.json new file mode 100644 index 0000000000..3822fadaaa --- /dev/null +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/events/event.json @@ -0,0 +1,63 @@ +{ + "body": "{\"message\": \"hello world\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + } + \ No newline at end of file diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/Function.cs b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/Function.cs index 32f4d6b7e0..e043338e6c 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/Function.cs +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-dotnet/{{cookiecutter.project_name}}/src/HelloWorld/Function.cs @@ -1,9 +1,7 @@ -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; @@ -25,20 +23,19 @@ private static async Task GetCallingIP() client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Add("User-Agent", "AWS Lambda .Net Client"); - var stringTask = client.GetStringAsync("http://checkip.amazonaws.com/").ConfigureAwait(continueOnCapturedContext:false); + var msg = await client.GetStringAsync("http://checkip.amazonaws.com/").ConfigureAwait(continueOnCapturedContext:false); - var msg = await stringTask; return msg.Replace("\n",""); } - public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) { - string location = GetCallingIP().Result; - Dictionary body = new Dictionary + var location = await GetCallingIP(); + var body = new Dictionary { { "message", "hello world" }, - { "location", location }, + { "location", location } }; return new APIGatewayProxyResponse 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 83c5801060..e8fa6c978b 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 @@ -3,11 +3,8 @@ using System.Linq; using System.Threading.Tasks; using System.Net.Http; -using System.Net.Http.Headers; - using Newtonsoft.Json; using Xunit; -using Amazon.Lambda.Core; using Amazon.Lambda.TestUtilities; using Amazon.Lambda.APIGatewayEvents; @@ -29,14 +26,10 @@ private static async Task GetCallingIP() } [Fact] - public void TestHelloWorldFunctionHandler() + public async Task TestHelloWorldFunctionHandler() { - TestLambdaContext context; - APIGatewayProxyRequest request; - APIGatewayProxyResponse response; - - request = new APIGatewayProxyRequest(); - context = new TestLambdaContext(); + var request = new APIGatewayProxyRequest(); + var context = new TestLambdaContext(); string location = GetCallingIP().Result; Dictionary body = new Dictionary { @@ -44,7 +37,7 @@ public void TestHelloWorldFunctionHandler() { "location", location }, }; - var ExpectedResponse = new APIGatewayProxyResponse + var expectedResponse = new APIGatewayProxyResponse { Body = JsonConvert.SerializeObject(body), StatusCode = 200, @@ -52,14 +45,14 @@ public void TestHelloWorldFunctionHandler() }; var function = new Function(); - response = function.FunctionHandler(request, context); + var response = await function.FunctionHandler(request, context); Console.WriteLine("Lambda Response: \n" + response.Body); - Console.WriteLine("Expected Response: \n" + ExpectedResponse.Body); + Console.WriteLine("Expected Response: \n" + expectedResponse.Body); - Assert.Equal(ExpectedResponse.Body, response.Body); - Assert.Equal(ExpectedResponse.Headers, response.Headers); - Assert.Equal(ExpectedResponse.StatusCode, response.StatusCode); + Assert.Equal(expectedResponse.Body, response.Body); + Assert.Equal(expectedResponse.Headers, response.Headers); + Assert.Equal(expectedResponse.StatusCode, response.StatusCode); } } } \ No newline at end of file diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-gradle/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-gradle/{{cookiecutter.project_name}}/README.md index daa130ff80..a1729ec9d0 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-gradle/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-gradle/{{cookiecutter.project_name}}/README.md @@ -1,146 +1,138 @@ # {{ cookiecutter.project_name }} -This is a sample template for {{ cookiecutter.project_name }} - Below is a brief explanation of what we have generated for you: +This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. -```bash -. -├── HelloWorldFunction -│   ├── build.gradle <-- Java Dependencies -│   ├── gradle <-- Gradle related Boilerplate -│   │   └── wrapper -│   │   ├── gradle-wrapper.jar -│   │   └── gradle-wrapper.properties -│   ├── gradlew <-- Linux/Mac Gradle Wrapper -│   ├── gradlew.bat <-- Windows Gradle Wrapper -│   └── src -│   ├── main -│   │   └── java -│   │   └── helloworld <-- Source code for a lambda function -│   │   ├── App.java <-- Lambda function code -│   │   └── GatewayResponse.java <-- POJO for API Gateway Responses object -│   └── test <-- Unit tests -│   └── java -│   └── helloworld -│   └── AppTest.java -├── README.md <-- This instructions file -└── template.yaml -``` +- HelloWorldFunction/src/main - Code for the application's Lambda function. +- events - Invocation events that you can use to invoke the function. +- HelloWorldFunction/src/test - Unit tests for the application code. +- template.yaml - A template that defines the application's AWS resources. + +The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. + +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. +The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. + +* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) -## Requirements +## Deploy the sample application -* AWS CLI already configured with Administrator permission -* [Java SE Development Kit 8 installed](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) -* [Docker installed](https://www.docker.com/community-edition) +The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. -## Setup process +To use the SAM CLI, you need the following tools. -### Installing dependencies +* AWS CLI - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) and [configure it with your AWS credentials]. +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* Java8 - [Install the Java SE Development Kit 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) + +The SAM CLI uses an Amazon S3 bucket to store your application's deployment artifacts. If you don't have a bucket suitable for this purpose, create one. Replace `BUCKET_NAME` in the commands in this section with a unique bucket name. ```bash -sam build +{{ cookiecutter.project_name }}$ aws s3 mb s3://BUCKET_NAME ``` -You can also build on a Lambda like environment by using: +To prepare the application for deployment, use the `sam package` command. ```bash -sam build --use-container +{{ cookiecutter.project_name }}$ sam package \ + --output-template-file packaged.yaml \ + --s3-bucket BUCKET_NAME ``` -### Local development +The SAM CLI creates deployment packages, uploads them to the S3 bucket, and creates a new version of the template that refers to the artifacts in the bucket. -**Invoking function locally through local API Gateway** +To deploy the application, use the `sam deploy` command. ```bash -sam local start-api +{{ cookiecutter.project_name }}$ sam deploy \ + --template-file packaged.yaml \ + --stack-name {{ cookiecutter.project_name }} \ + --capabilities CAPABILITY_IAM ``` -If the previous command ran successfully you should now be able to hit the following local endpoint to invoke your function `http://localhost:3000/hello` - -**SAM CLI** is used to emulate both Lambda and API Gateway locally and uses our `template.yaml` to understand how to bootstrap this environment (runtime, where the source code is, etc.) - The following excerpt is what the CLI will read in order to initialize an API and its routes: +After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: -```yaml -... -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 -``` +```bash +{{ cookiecutter.project_name }}$ aws cloudformation describe-stacks \ + --stack-name {{ cookiecutter.project_name }} \ + --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ + --output table +``` -## Packaging and deployment +## Use the SAM CLI to build and test locally -Firstly, we need a `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: +Build your application with the `sam build` command. ```bash -aws s3 mb s3://BUCKET_NAME +{{ cookiecutter.project_name }}$ sam build ``` -Next, run the following command to package our Lambda function to S3: +The SAM CLI installs dependencies defined in `HelloWorldFunction/build.gradle`, creates a deployment package, and saves it in the `.aws-sam/build` folder. + +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. + +Run functions locally and invoke them with the `sam local invoke` command. ```bash -sam package \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME +{{ cookiecutter.project_name }}$ sam local invoke HelloWorldFunction --event events/event.json ``` -Next, the following command will create a Cloudformation Stack and deploy your SAM resources. +The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. ```bash -sam deploy \ - --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --capabilities CAPABILITY_IAM +{{ cookiecutter.project_name }}$ sam local start-api +{{ cookiecutter.project_name }}$ curl http://localhost:3000/ ``` -> **See [Serverless Application Model (SAM) HOWTO Guide](https://github.com/awslabs/serverless-application-model/blob/master/HOWTO.md) for more details in how to get started.** - -After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: +The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. -```bash -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --query 'Stacks[].Outputs' +```yaml + Events: + HelloWorld: + Type: Api + Properties: + Path: /hello + Method: get ``` -## Testing +## Add a resource to your application +The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. -We use `JUnit` for testing our code and you can simply run the following command to run our tests: +## Fetch, tail, and filter Lambda function logs + +To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. + +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. ```bash -gradle test +{{ cookiecutter.project_name }}$ sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name }} --tail ``` -# Appendix +You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). -## AWS CLI commands +## Unit tests -AWS CLI commands to package, deploy and describe outputs defined within the cloudformation stack: +Tests are defined in the `HelloWorldFunction/src/test` folder in this project. ```bash -sam package \ - --template-file template.yaml \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME - -sam 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' +{{ cookiecutter.project_name }}$ cd HelloWorldFunction +HelloWorldFunction$ gradle test ``` -## Bringing to the next level +## Cleanup + +To delete the sample application and the bucket that you created, use the AWS CLI. -Here are a few ideas that you can use to get more acquainted as to how this overall process works: +```bash +{{ cookiecutter.project_name }}$ aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name }} +{{ cookiecutter.project_name }}$ aws s3 rb s3://BUCKET_NAME +``` -* Create an additional API resource (e.g. /hello/{proxy+}) and return the name requested through this new path -* Update unit test to capture that -* Package & Deploy +## Resources -Next, you can use the following resources to know more about beyond hello world samples and how others structure their Serverless applications: +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. -* [AWS Serverless Application Repository](https://aws.amazon.com/serverless/serverlessrepo/) +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-gradle/{{cookiecutter.project_name}}/events/event.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-gradle/{{cookiecutter.project_name}}/events/event.json new file mode 100644 index 0000000000..3822fadaaa --- /dev/null +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-gradle/{{cookiecutter.project_name}}/events/event.json @@ -0,0 +1,63 @@ +{ + "body": "{\"message\": \"hello world\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + } + \ No newline at end of file diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-maven/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-maven/{{cookiecutter.project_name}}/README.md index 0d52d9f69c..5b46ee17f5 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-maven/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-maven/{{cookiecutter.project_name}}/README.md @@ -1,152 +1,139 @@ # {{ cookiecutter.project_name }} -This is a sample template for {{ cookiecutter.project_name }} - Below is a brief explanation of what we have generated for you: +This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. -```bash -├── README.md <-- This instructions file -├── HelloWorldFunction <-- Source for HelloWorldFunction Lambda Function -│ ├── pom.xml <-- Java dependencies -│ └── src -│ ├── main -│ │ └── java -│ │ └── helloworld -│ │ ├── App.java <-- Lambda function code -│ │ └── GatewayResponse.java <-- POJO for API Gateway Responses object -│ └── test <-- Unit tests -│ └── java -│ └── helloworld -│ └── AppTest.java -└── template.yaml -``` +- HelloWorldFunction/src/main - Code for the application's Lambda function. +- events - Invocation events that you can use to invoke the function. +- HelloWorldFunction/src/test - Unit tests for the application code. +- template.yaml - A template that defines the application's AWS resources. -## Requirements +The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. -* AWS CLI already configured with Administrator permission -* [Java SE Development Kit 8 installed](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) -* [Docker installed](https://www.docker.com/community-edition) -* [Maven](https://maven.apache.org/install.html) +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. +The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. -## Setup process +* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) -### Installing dependencies +## Deploy the sample application -```bash -sam build -``` +The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. + +To use the SAM CLI, you need the following tools. -You can also build on a Lambda like environment by using +* AWS CLI - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) and [configure it with your AWS credentials]. +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* Java8 - [Install the Java SE Development Kit 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +* Maven - [Install Maven](https://maven.apache.org/install.html) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) + +The SAM CLI uses an Amazon S3 bucket to store your application's deployment artifacts. If you don't have a bucket suitable for this purpose, create one. Replace `BUCKET_NAME` in the commands in this section with a unique bucket name. ```bash -sam build --use-container +{{ cookiecutter.project_name }}$ aws s3 mb s3://BUCKET_NAME ``` -### Local development - -**Invoking function locally through local API Gateway** +To prepare the application for deployment, use the `sam package` command. ```bash -sam local start-api +{{ cookiecutter.project_name }}$ sam package \ + --output-template-file packaged.yaml \ + --s3-bucket BUCKET_NAME ``` -If the previous command ran successfully you should now be able to hit the following local endpoint to invoke your function `http://localhost:3000/hello` +The SAM CLI creates deployment packages, uploads them to the S3 bucket, and creates a new version of the template that refers to the artifacts in the bucket. -**SAM CLI** is used to emulate both Lambda and API Gateway locally and uses our `template.yaml` to understand how to bootstrap this environment (runtime, where the source code is, etc.) - The following excerpt is what the CLI will read in order to initialize an API and its routes: +To deploy the application, use the `sam deploy` command. -```yaml -... -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 +```bash +{{ cookiecutter.project_name }}$ sam deploy \ + --template-file packaged.yaml \ + --stack-name {{ cookiecutter.project_name }} \ + --capabilities CAPABILITY_IAM ``` -## Packaging and deployment +After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: -AWS Lambda Java runtime accepts either a zip file or a standalone JAR file - We use the latter in this example. SAM will use `CodeUri` property to know where to look up for both application and dependencies: +```bash +{{ cookiecutter.project_name }}$ aws cloudformation describe-stacks \ + --stack-name {{ cookiecutter.project_name }} \ + --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ + --output table +``` -```yaml -... - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: target/HelloWorld-1.0.jar - Handler: helloworld.App::handleRequest -``` +## Use the SAM CLI to build and test locally -Firstly, we need a `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: +Build your application with the `sam build` command. ```bash -aws s3 mb s3://BUCKET_NAME +{{ cookiecutter.project_name }}$ sam build ``` -Next, run the following command to package our Lambda function to S3: +The SAM CLI installs dependencies defined in `HelloWorldFunction/pom.xml`, creates a deployment package, and saves it in the `.aws-sam/build` folder. + +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. + +Run functions locally and invoke them with the `sam local invoke` command. ```bash -sam package \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME +{{ cookiecutter.project_name }}$ sam local invoke HelloWorldFunction --event events/event.json ``` -Next, the following command will create a Cloudformation Stack and deploy your SAM resources. +The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. ```bash -sam deploy \ - --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --capabilities CAPABILITY_IAM +{{ cookiecutter.project_name }}$ sam local start-api +{{ cookiecutter.project_name }}$ curl http://localhost:3000/ ``` -> **See [Serverless Application Model (SAM) HOWTO Guide](https://github.com/awslabs/serverless-application-model/blob/master/HOWTO.md) for more details in how to get started.** - -After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: +The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. -```bash -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --query 'Stacks[].Outputs' +```yaml + Events: + HelloWorld: + Type: Api + Properties: + Path: /hello + Method: get ``` -## Testing +## Add a resource to your application +The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. -We use `JUnit` for testing our code and you can simply run the following command to run our tests: +## Fetch, tail, and filter Lambda function logs + +To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. + +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. ```bash -cd HelloWorldFunction -mvn test +{{ cookiecutter.project_name }}$ sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name }} --tail ``` -# Appendix +You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). -## AWS CLI commands +## Unit tests -AWS CLI commands to package, deploy and describe outputs defined within the cloudformation stack: +Tests are defined in the `HelloWorldFunction/src/test` folder in this project. ```bash -sam package \ - --template-file template.yaml \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME - -sam 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' +{{ cookiecutter.project_name }}$ cd HelloWorldFunction +HelloWorldFunction$ mvn test ``` -## Bringing to the next level +## Cleanup + +To delete the sample application and the bucket that you created, use the AWS CLI. -Here are a few ideas that you can use to get more acquainted as to how this overall process works: +```bash +{{ cookiecutter.project_name }}$ aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name }} +{{ cookiecutter.project_name }}$ aws s3 rb s3://BUCKET_NAME +``` -* Create an additional API resource (e.g. /hello/{proxy+}) and return the name requested through this new path -* Update unit test to capture that -* Package & Deploy +## Resources -Next, you can use the following resources to know more about beyond hello world samples and how others structure their Serverless applications: +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. -* [AWS Serverless Application Repository](https://aws.amazon.com/serverless/serverlessrepo/) +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-maven/{{cookiecutter.project_name}}/events/event.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-maven/{{cookiecutter.project_name}}/events/event.json new file mode 100644 index 0000000000..3822fadaaa --- /dev/null +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-java-maven/{{cookiecutter.project_name}}/events/event.json @@ -0,0 +1,63 @@ +{ + "body": "{\"message\": \"hello world\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + } + \ No newline at end of file diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/README.md index b5bdc89e6b..baab85559a 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/README.md @@ -1,211 +1,139 @@ # {{ cookiecutter.project_name }} -This is a sample template for {{ cookiecutter.project_name }} - Below is a brief explanation of what we have generated for you: +This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. -```bash -. -├── README.MD <-- This instructions file -├── event.json <-- API Gateway Proxy Integration event payload -├── hello-world <-- Source code for a lambda function -│ └── app.js <-- Lambda function code -│ └── package.json <-- NodeJS dependencies and scripts -│ └── tests <-- Unit tests -│ └── unit -│ └── test-handler.js -├── template.yaml <-- SAM template -``` - -## Requirements - -* AWS CLI already configured with Administrator permission -{%- if cookiecutter.runtime == 'nodejs8.10' %} -* [NodeJS 8.10+ installed](https://nodejs.org/en/download/releases/) -{%- else %} -* [NodeJS 10.10+ installed](https://nodejs.org/en/download/releases/) -{%- endif %} +- hello-world - Code for the application's Lambda function. +- events - Invocation events that you can use to invoke the function. +- hello-world/tests - Unit tests for the application code. +- template.yaml - A template that defines the application's AWS resources. -* [Docker installed](https://www.docker.com/community-edition) +The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. -## Setup process +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. +The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. -### Local development - -**Invoking function locally using a local sample payload** - -```bash -sam local invoke HelloWorldFunction --event event.json -``` - -**Invoking function locally through local API Gateway** +* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) -```bash -sam local start-api -``` +## Deploy the sample application -If the previous command ran successfully you should now be able to hit the following local endpoint to invoke your function `http://localhost:3000/hello` +The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. -**SAM CLI** is used to emulate both Lambda and API Gateway locally and uses our `template.yaml` to understand how to bootstrap this environment (runtime, where the source code is, etc.) - The following excerpt is what the CLI will read in order to initialize an API and its routes: +To use the SAM CLI, you need the following tools. -```yaml -... -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 -``` +* AWS CLI - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) and [configure it with your AWS credentials]. +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* Node.js - [Install Node.js 10](https://nodejs.org/en/), including the NPM package management tool. +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) -## Packaging and deployment - -AWS Lambda NodeJS 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: hello-world/ - ... -``` - -Firstly, we need a `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: +The SAM CLI uses an Amazon S3 bucket to store your application's deployment artifacts. If you don't have a bucket suitable for this purpose, create one. Replace `BUCKET_NAME` in the commands in this section with a unique bucket name. ```bash -aws s3 mb s3://BUCKET_NAME +{{ cookiecutter.project_name }}$ aws s3 mb s3://BUCKET_NAME ``` -Next, run the following command to package our Lambda function to S3: +To prepare the application for deployment, use the `sam package` command. ```bash -sam package \ +{{ cookiecutter.project_name }}$ sam package \ --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME + --s3-bucket BUCKET_NAME ``` -Next, the following command will create a Cloudformation Stack and deploy your SAM resources. +The SAM CLI creates deployment packages, uploads them to the S3 bucket, and creates a new version of the template that refers to the artifacts in the bucket. + +To deploy the application, use the `sam deploy` command. ```bash -sam deploy \ +{{ cookiecutter.project_name }}$ sam deploy \ --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ + --stack-name {{ cookiecutter.project_name }} \ --capabilities CAPABILITY_IAM ``` -> **See [Serverless Application Model (SAM) HOWTO Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-quick-start.html) for more details in how to get started.** - After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: ```bash -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ +{{ cookiecutter.project_name }}$ aws cloudformation describe-stacks \ + --stack-name {{ cookiecutter.project_name }} \ --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ --output table ``` -## Fetch, tail, and filter Lambda function logs - -To simplify troubleshooting, SAM CLI has a command called sam logs. sam logs lets you fetch logs generated by your Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. +## Use the SAM CLI to build and test locally -`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. +Build your application with the `sam build` command. ```bash -sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} --tail +{{ cookiecutter.project_name }}$ sam build ``` -You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). +The SAM CLI installs dependencies defined in `hello-world/package.json`, creates a deployment package, and saves it in the `.aws-sam/build` folder. -## Testing +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. -We use `mocha` for testing our code and it is already added in `package.json` under `scripts`, so that we can simply run the following command to run our tests: +Run functions locally and invoke them with the `sam local invoke` command. ```bash -cd hello-world -npm install -npm run test +{{ cookiecutter.project_name }}$ sam local invoke putItemFunction --event events/event.json ``` -## Cleanup - -In order to delete our Serverless Application recently deployed you can use the following AWS CLI Command: +The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. ```bash -aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} +{{ cookiecutter.project_name }}$ sam local start-api +{{ cookiecutter.project_name }}$ curl http://localhost:3000/ ``` -## Bringing to the next level +The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. -Here are a few things you can try to get more acquainted with building serverless applications using SAM: - -### Learn how SAM Build can help you with dependencies - -* Uncomment lines on `app.js` -* Build the project with ``sam build --use-container`` -* Invoke with ``sam local invoke HelloWorldFunction --event event.json`` -* Update tests - -### Create an additional API resource - -* Create a catch all resource (e.g. /hello/{proxy+}) and return the name requested through this new path -* Update tests - -### Step-through debugging - -* **[Enable step-through debugging docs for supported runtimes]((https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-debugging.html))** - -Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) +```yaml + Events: + HelloWorld: + Type: Api + Properties: + Path: /hello + Method: get +``` -# Appendix +## Add a resource to your application +The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. -## Building the project +## Fetch, tail, and filter Lambda function logs -[AWS Lambda requires a flat folder](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-create-deployment-pkg.html) with the application as well as its dependencies in a node_modules folder. When you make changes to your source code or dependency manifest, -run the following command to build your project local testing and deployment: +To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. -```bash -sam build -``` +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. -If your dependencies contain native modules that need to be compiled specifically for the operating system running on AWS Lambda, use this command to build inside a Lambda-like Docker container instead: ```bash -sam build --use-container +{{ cookiecutter.project_name }}$ sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name }} --tail ``` -By default, this command writes built artifacts to `.aws-sam/build` folder. +You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). -## SAM and AWS CLI commands +## Unit tests -All commands used throughout this document +Tests are defined in the `hello-world/tests` folder in this project. Use NPM to install the [Mocha test framework](https://mochajs.org/) and run unit tests. ```bash -# Invoke function locally with event.json as an input -sam local invoke HelloWorldFunction --event event.json - -# Run API Gateway locally -sam local start-api +{{ cookiecutter.project_name }}$ cd hello-world +hello-world$ npm install +hello-world$ npm run test +``` -# Create S3 bucket -aws s3 mb s3://BUCKET_NAME +## Cleanup -# Package Lambda function defined locally and upload to S3 as an artifact -sam package \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME +To delete the sample application and the bucket that you created, use the AWS CLI. -# Deploy SAM template as a CloudFormation stack -sam deploy \ - --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --capabilities CAPABILITY_IAM +```bash +{{ cookiecutter.project_name }}$ aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name }} +{{ cookiecutter.project_name }}$ aws s3 rb s3://BUCKET_NAME +``` -# Describe Output section of CloudFormation stack previously created -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ - --output table +## Resources -# Tail Lambda function Logs using Logical name defined in SAM Template -sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} --tail -``` +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. -**NOTE**: Alternatively this could be part of package.json scripts section. +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) \ No newline at end of file diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/event.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/events/event.json similarity index 100% rename from samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/event.json rename to samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs/{{cookiecutter.project_name}}/events/event.json diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/README.md index 513a84f724..a12489cb39 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/README.md @@ -1,214 +1,142 @@ # {{ cookiecutter.project_name }} -This is a sample template for {{ cookiecutter.project_name }} - Below is a brief explanation of what we have generated for you: +This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. -```bash -. -├── README.md <-- This instructions file -├── event.json <-- API Gateway Proxy Integration event payload -├── hello_world <-- Source code for a lambda function -│   ├── __init__.py -│   ├── app.py <-- Lambda function code -│   ├── requirements.txt <-- Lambda function code -├── template.yaml <-- SAM Template -└── tests <-- Unit tests - └── unit - ├── __init__.py - └── test_handler.py -``` - -## Requirements - -* AWS CLI already configured with Administrator permission -{%- if cookiecutter.runtime == 'python2.7' %} -* [Python 2.7 installed](https://www.python.org/downloads/) -{%- else %} -* [Python 3 installed](https://www.python.org/downloads/) -{%- endif %} -* [Docker installed](https://www.docker.com/community-edition) +- hello_world - Code for the application's Lambda function. +- events - Invocation events that you can use to invoke the function. +- tests - Unit tests for the application code. +- template.yaml - A template that defines the application's AWS resources. -## Setup process - -### Local development - -**Invoking function locally using a local sample payload** - -```bash -sam local invoke HelloWorldFunction --event event.json -``` - -**Invoking function locally through local API Gateway** - -```bash -sam local start-api -``` +The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. -If the previous command ran successfully you should now be able to hit the following local endpoint to invoke your function `http://localhost:3000/hello` +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. +The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. -**SAM CLI** is used to emulate both Lambda and API Gateway locally and uses our `template.yaml` to understand how to bootstrap this environment (runtime, where the source code is, etc.) - The following excerpt is what the CLI will read in order to initialize an API and its routes: +* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) -```yaml -... -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 -``` +## Deploy the sample application -## Packaging and deployment +The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. -AWS Lambda Python 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: +To use the SAM CLI, you need the following tools. -```yaml -... - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: hello_world/ - ... -``` +* AWS CLI - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) and [configure it with your AWS credentials]. +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +{%- if cookiecutter.runtime == 'python2.7' %} +* [Python 2.7 installed](https://www.python.org/downloads/) +{%- else %} +* [Python 3 installed](https://www.python.org/downloads/) +{%- endif %} +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) -Firstly, we need a `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: +The SAM CLI uses an Amazon S3 bucket to store your application's deployment artifacts. If you don't have a bucket suitable for this purpose, create one. Replace `BUCKET_NAME` in the commands in this section with a unique bucket name. ```bash -aws s3 mb s3://BUCKET_NAME +{{ cookiecutter.project_name }}$ aws s3 mb s3://BUCKET_NAME ``` -Next, run the following command to package our Lambda function to S3: +To prepare the application for deployment, use the `sam package` command. ```bash -sam package \ +{{ cookiecutter.project_name }}$ sam package \ --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME + --s3-bucket BUCKET_NAME ``` -Next, the following command will create a Cloudformation Stack and deploy your SAM resources. +The SAM CLI creates deployment packages, uploads them to the S3 bucket, and creates a new version of the template that refers to the artifacts in the bucket. + +To deploy the application, use the `sam deploy` command. ```bash -sam deploy \ +{{ cookiecutter.project_name }}$ sam deploy \ --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ + --stack-name {{ cookiecutter.project_name }} \ --capabilities CAPABILITY_IAM ``` -> **See [Serverless Application Model (SAM) HOWTO Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-quick-start.html) for more details in how to get started.** - After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: ```bash -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ +{{ cookiecutter.project_name }}$ aws cloudformation describe-stacks \ + --stack-name {{ cookiecutter.project_name }} \ --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ --output table ``` -## Fetch, tail, and filter Lambda function logs - -To simplify troubleshooting, SAM CLI has a command called sam logs. sam logs lets you fetch logs generated by your Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. +## Use the SAM CLI to build and test locally -`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. +Build your application with the `sam build` command. ```bash -sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} --tail +{{ cookiecutter.project_name }}$ sam build ``` -You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). - -## Testing +The SAM CLI installs dependencies defined in `hello_world/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder. +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. -Next, we install test dependencies and we run `pytest` against our `tests` folder to run our initial unit tests: +Run functions locally and invoke them with the `sam local invoke` command. ```bash -pip install pytest pytest-mock --user -python -m pytest tests/ -v +{{ cookiecutter.project_name }}$ sam local invoke HelloWorldFunction --event events/event.json ``` -## Cleanup - -In order to delete our Serverless Application recently deployed you can use the following AWS CLI Command: +The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. ```bash -aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} +{{ cookiecutter.project_name }}$ sam local start-api +{{ cookiecutter.project_name }}$ curl http://localhost:3000/ ``` -## Bringing to the next level - -Here are a few things you can try to get more acquainted with building serverless applications using SAM: - -### Learn how SAM Build can help you with dependencies - -* Uncomment lines on `app.py` -* Build the project with ``sam build --use-container`` -* Invoke with ``sam local invoke HelloWorldFunction --event event.json`` -* Update tests - -### Create an additional API resource - -* Create a catch all resource (e.g. /hello/{proxy+}) and return the name requested through this new path -* Update tests +The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. -### Step-through debugging - -* **[Enable step-through debugging docs for supported runtimes]((https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-debugging.html))** - -Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) +```yaml + Events: + HelloWorld: + Type: Api + Properties: + Path: /hello + Method: get +``` -# Appendix +## Add a resource to your application +The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. -## Building the project +## Fetch, tail, and filter Lambda function logs -[AWS Lambda requires a flat folder](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html) with the application as well as its dependencies in deployment package. When you make changes to your source code or dependency manifest, -run the following command to build your project local testing and deployment: +To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. -```bash -sam build -``` +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. -If your dependencies contain native modules that need to be compiled specifically for the operating system running on AWS Lambda, use this command to build inside a Lambda-like Docker container instead: ```bash -sam build --use-container +{{ cookiecutter.project_name }}$ sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name }} --tail ``` -By default, this command writes built artifacts to `.aws-sam/build` folder. +You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). -## SAM and AWS CLI commands +## Unit tests -All commands used throughout this document +Tests are defined in the `tests` folder in this project. Use PIP to install the [pytest](https://docs.pytest.org/en/latest/) and run unit tests. ```bash -# Generate event.json via generate-event command -sam local generate-event apigateway aws-proxy > event.json - -# Invoke function locally with event.json as an input -sam local invoke HelloWorldFunction --event event.json - -# Run API Gateway locally -sam local start-api +{{ cookiecutter.project_name }}$ pip install pytest pytest-mock --user +{{ cookiecutter.project_name }}$ python -m pytest tests/ -v +``` -# Create S3 bucket -aws s3 mb s3://BUCKET_NAME +## Cleanup -# Package Lambda function defined locally and upload to S3 as an artifact -sam package \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME +To delete the sample application and the bucket that you created, use the AWS CLI. -# Deploy SAM template as a CloudFormation stack -sam deploy \ - --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --capabilities CAPABILITY_IAM +```bash +{{ cookiecutter.project_name }}$ aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name }} +{{ cookiecutter.project_name }}$ aws s3 rb s3://BUCKET_NAME +``` -# Describe Output section of CloudFormation stack previously created -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ - --output table +## Resources -# Tail Lambda function Logs using Logical name defined in SAM Template -sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} --tail -``` +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/event.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/events/event.json similarity index 100% rename from samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/event.json rename to samcli/local/init/templates/cookiecutter-aws-sam-hello-python/{{cookiecutter.project_name}}/events/event.json diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/README.md index 8c6ebb70fc..8c426579f1 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/README.md @@ -1,209 +1,137 @@ # {{ cookiecutter.project_name }} -This is a sample template for {{ cookiecutter.project_name }} - Below is a brief explanation of what we have generated for you: +This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. -```bash -. -├── README.md <-- This instructions file -├── event.json <-- API Gateway Proxy Integration event payload -├── hello_world <-- Source code for a lambda function -│ ├── app.rb <-- Lambda function code -│ ├── Gemfile <-- Ruby function dependencies -├── template.yaml <-- SAM template -├── Gemfile <-- Ruby test/documentation dependencies -└── tests <-- Unit tests - └── unit - └── test_handler.rb -``` - -## Requirements - -* AWS CLI already configured with at Administrator permission -* [Ruby 2.5 installed](https://www.ruby-lang.org/en/documentation/installation/) -* [Docker installed](https://www.docker.com/community-edition) +- hello_world - Code for the application's Lambda function. +- events - Invocation events that you can use to invoke the function. +- tests - Unit tests for the application code. +- template.yaml - A template that defines the application's AWS resources. -## Setup process +The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. -### Local development - -**Invoking function locally using a local sample payload** - -```bash -sam local invoke HelloWorldFunction --event event.json -``` +If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. +The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. -**Invoking function locally through local API Gateway** +* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) +* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) +* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) -```bash -sam local start-api -``` - -If the previous command ran successfully you should now be able to hit the following local endpoint to invoke your function `http://localhost:3000/hello` - -**SAM CLI** is used to emulate both Lambda and API Gateway locally and uses our `template.yaml` to understand how to bootstrap this environment (runtime, where the source code is, etc.) - The following excerpt is what the CLI will read in order to initialize an API and its routes: - -```yaml -... -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 -``` +## Deploy the sample application -## Packaging and deployment +The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. -AWS Lambda Ruby 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: +To use the SAM CLI, you need the following tools. -```yaml -... - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: hello_world/ - ... -``` +* AWS CLI - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) and [configure it with your AWS credentials]. +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* Ruby - [Install Ruby 2.5](https://www.ruby-lang.org/en/documentation/installation/) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) -Firstly, we need a `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: +The SAM CLI uses an Amazon S3 bucket to store your application's deployment artifacts. If you don't have a bucket suitable for this purpose, create one. Replace `BUCKET_NAME` in the commands in this section with a unique bucket name. ```bash -aws s3 mb s3://BUCKET_NAME +{{ cookiecutter.project_name }}$ aws s3 mb s3://BUCKET_NAME ``` -Next, run the following command to package our Lambda function to S3: +To prepare the application for deployment, use the `sam package` command. ```bash -sam package \ +{{ cookiecutter.project_name }}$ sam package \ --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME + --s3-bucket BUCKET_NAME ``` -Next, the following command will create a Cloudformation Stack and deploy your SAM resources. +The SAM CLI creates deployment packages, uploads them to the S3 bucket, and creates a new version of the template that refers to the artifacts in the bucket. + +To deploy the application, use the `sam deploy` command. ```bash -sam deploy \ +{{ cookiecutter.project_name }}$ sam deploy \ --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ + --stack-name {{ cookiecutter.project_name }} \ --capabilities CAPABILITY_IAM ``` -> **See [Serverless Application Model (SAM) HOWTO Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-quick-start.html) for more details in how to get started.** - After deployment is complete you can run the following command to retrieve the API Gateway Endpoint URL: ```bash -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ +{{ cookiecutter.project_name }}$ aws cloudformation describe-stacks \ + --stack-name {{ cookiecutter.project_name }} \ --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ --output table ``` -## Fetch, tail, and filter Lambda function logs +## Use the SAM CLI to build and test locally -To simplify troubleshooting, SAM CLI has a command called sam logs. sam logs lets you fetch logs generated by your Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. - -`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. +Build your application with the `sam build` command. ```bash -sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} --tail +{{ cookiecutter.project_name }}$ sam build ``` -You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). +The SAM CLI installs dependencies defined in `hello_world/Gemfile`, creates a deployment package, and saves it in the `.aws-sam/build` folder. -## Testing +Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. -Run our initial unit tests: +Run functions locally and invoke them with the `sam local invoke` command. ```bash -ruby tests/unit/test_handler.rb +{{ cookiecutter.project_name }}$ sam local invoke HelloWorldFunction --event events/event.json ``` -## Cleanup - -In order to delete our Serverless Application recently deployed you can use the following AWS CLI Command: +The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. ```bash -aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} +{{ cookiecutter.project_name }}$ sam local start-api +{{ cookiecutter.project_name }}$ curl http://localhost:3000/ ``` -## Bringing to the next level - -Here are a few things you can try to get more acquainted with building serverless applications using SAM: - -### Learn how SAM Build can help you with dependencies - -* Uncomment lines on `app.rb` -* Build the project with ``sam build --use-container`` -* Invoke with ``sam local invoke HelloWorldFunction --event event.json`` -* Update tests - -### Create an additional API resource - -* Create a catch all resource (e.g. /hello/{proxy+}) and return the name requested through this new path -* Update tests - -### Step-through debugging - -* **[Enable step-through debugging docs for supported runtimes]((https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-debugging.html))** - -Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) +The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. +```yaml + Events: + HelloWorld: + Type: Api + Properties: + Path: /hello + Method: get +``` -# Appendix +## Add a resource to your application +The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. -## Building the project +## Fetch, tail, and filter Lambda function logs -[AWS Lambda requires a flat folder](https://docs.aws.amazon.com/lambda/latest/dg/ruby-package.html) with the application as well as its dependencies. When you make changes to your source code or dependency manifest, -run the following command to build your project local testing and deployment: +To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. -```bash -sam build -``` +`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. -If your dependencies contain native modules that need to be compiled specifically for the operating system running on AWS Lambda, use this command to build inside a Lambda-like Docker container instead: ```bash -sam build --use-container +{{ cookiecutter.project_name }}$ sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name }} --tail ``` -By default, this command writes built artifacts to `.aws-sam/build` folder. - +You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). -## SAM and AWS CLI commands +## Unit tests -All commands used throughout this document +Tests are defined in the `tests` folder in this project. ```bash -# Generate event.json via generate-event command -sam local generate-event apigateway aws-proxy > event.json - -# Invoke function locally with event.json as an input -sam local invoke HelloWorldFunction --event event.json - -# Run API Gateway locally -sam local start-api +{{ cookiecutter.project_name }}$ ruby tests/unit/test_handler.rb +``` -# Create S3 bucket -aws s3 mb s3://BUCKET_NAME +## Cleanup -# Package Lambda function defined locally and upload to S3 as an artifact -sam package \ - --output-template-file packaged.yaml \ - --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME +To delete the sample application and the bucket that you created, use the AWS CLI. -# Deploy SAM template as a CloudFormation stack -sam deploy \ - --template-file packaged.yaml \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --capabilities CAPABILITY_IAM +```bash +{{ cookiecutter.project_name }}$ aws cloudformation delete-stack --stack-name {{ cookiecutter.project_name }} +{{ cookiecutter.project_name }}$ aws s3 rb s3://BUCKET_NAME +``` -# Describe Output section of CloudFormation stack previously created -aws cloudformation describe-stacks \ - --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} \ - --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \ - --output table +## Resources -# Tail Lambda function Logs using Logical name defined in SAM Template -sam logs -n HelloWorldFunction --stack-name {{ cookiecutter.project_name.lower().replace(' ', '-') }} --tail -``` +See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. +Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/event.json b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/events/event.json similarity index 100% rename from samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/event.json rename to samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/events/event.json diff --git a/tests/functional/commands/local/lib/test_local_api_service.py b/tests/functional/commands/local/lib/test_local_api_service.py index a507304bae..23df3e9025 100644 --- a/tests/functional/commands/local/lib/test_local_api_service.py +++ b/tests/functional/commands/local/lib/test_local_api_service.py @@ -10,6 +10,8 @@ import time import logging +from samcli.commands.local.lib.provider import Api +from samcli.local.apigw.local_apigw_service import Route from samcli.commands.local.lib import provider from samcli.commands.local.lib.local_lambda import LocalLambdaRunner from samcli.local.lambdafn.runtime import LambdaRuntime @@ -42,7 +44,7 @@ def setUp(self): self.static_dir = "mystaticdir" self.static_file_name = "myfile.txt" self.static_file_content = "This is a static file" - self._setup_static_file(os.path.join(self.cwd, self.static_dir), # Create static directory with in cwd + self._setup_static_file(os.path.join(self.cwd, self.static_dir), # Create static directory with in cwd self.static_file_name, self.static_file_content) @@ -56,12 +58,14 @@ def setUp(self): self.mock_function_provider.get.return_value = self.function # Setup two APIs pointing to the same function - apis = [ - provider.Api(path="/get", method="GET", function_name=self.function_name, cors="cors"), - provider.Api(path="/post", method="POST", function_name=self.function_name, cors="cors"), + routes = [ + Route(path="/get", methods=["GET"], function_name=self.function_name), + Route(path="/post", methods=["POST"], function_name=self.function_name), ] + api = Api(routes=routes) + self.api_provider_mock = Mock() - self.api_provider_mock.get_all.return_value = apis + self.api_provider_mock.get_all.return_value = api # Now wire up the Lambda invoker and pass it through the context self.lambda_invoke_context_mock = Mock() @@ -69,7 +73,9 @@ def setUp(self): layer_downloader = LayerDownloader("./", "./") lambda_image = LambdaImage(layer_downloader, False, False) local_runtime = LambdaRuntime(manager, lambda_image) - lambda_runner = LocalLambdaRunner(local_runtime, self.mock_function_provider, self.cwd, env_vars_values=None, + lambda_runner = LocalLambdaRunner(local_runtime, + self.mock_function_provider, + self.cwd, debug_context=None) self.lambda_invoke_context_mock.local_lambda_runner = lambda_runner self.lambda_invoke_context_mock.get_cwd.return_value = self.cwd @@ -77,7 +83,7 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.code_abs_path) - @patch("samcli.commands.local.lib.local_api_service.SamApiProvider") + @patch("samcli.commands.local.lib.sam_api_provider.SamApiProvider") def test_must_start_service_and_serve_endpoints(self, sam_api_provider_mock): sam_api_provider_mock.return_value = self.api_provider_mock @@ -97,7 +103,7 @@ def test_must_start_service_and_serve_endpoints(self, sam_api_provider_mock): response = requests.get(self.url + '/post') self.assertEquals(response.status_code, 403) # "HTTP GET /post" must not exist - @patch("samcli.commands.local.lib.local_api_service.SamApiProvider") + @patch("samcli.commands.local.lib.sam_api_provider.SamApiProvider") def test_must_serve_static_files(self, sam_api_provider_mock): sam_api_provider_mock.return_value = self.api_provider_mock @@ -123,10 +129,8 @@ def _start_service_thread(service): @staticmethod def _setup_static_file(directory, filename, contents): - if not os.path.isdir(directory): os.mkdir(directory) with open(os.path.join(directory, filename), "w") as fp: fp.write(contents) - diff --git a/tests/functional/commands/validate/lib/models/alexa_skill.yaml b/tests/functional/commands/validate/lib/models/alexa_skill.yaml new file mode 100644 index 0000000000..f8d81b0a7a --- /dev/null +++ b/tests/functional/commands/validate/lib/models/alexa_skill.yaml @@ -0,0 +1,19 @@ +# File: sam.yml +# Version: 0.9 + +AWSTemplateFormatVersion: '2010-09-09' +Parameters: {} +Resources: + AlexaSkillFunc: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Description: Created by SAM + Events: + AlexaSkillEvent: + Type: AlexaSkill + Handler: index.handler + MemorySize: 1024 + Runtime: nodejs4.3 + Timeout: 3 + diff --git a/tests/functional/commands/validate/lib/models/alexa_skill_with_skill_id.yaml b/tests/functional/commands/validate/lib/models/alexa_skill_with_skill_id.yaml new file mode 100644 index 0000000000..1714edb46e --- /dev/null +++ b/tests/functional/commands/validate/lib/models/alexa_skill_with_skill_id.yaml @@ -0,0 +1,20 @@ +# File: sam.yml +# Version: 0.9 + +AWSTemplateFormatVersion: '2010-09-09' +Parameters: {} +Resources: + AlexaSkillFunc: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Description: Created by SAM + Events: + AlexaSkillEvent: + Type: AlexaSkill + Properties: + SkillId: amzn1.ask.skill.12345678-1234-1234-1234-123456789 + Handler: index.handler + MemorySize: 1024 + Runtime: nodejs4.3 + Timeout: 3 diff --git a/tests/functional/commands/validate/lib/models/all_policy_templates.yaml b/tests/functional/commands/validate/lib/models/all_policy_templates.yaml new file mode 100644 index 0000000000..2cca6996f8 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/all_policy_templates.yaml @@ -0,0 +1,148 @@ +# "Kitchen Sink" test containing all supported policy templates. The idea is to know every one of them is +# transformable and fail on any changes in the policy template definition without updating the test +# Since this not about testing the transformation logic, we will keep the policy template parameter values as literal +# string + +Resources: + KitchenSinkFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Policies: + + - SQSPollerPolicy: + QueueName: name + + - LambdaInvokePolicy: + FunctionName: name + + - CloudWatchPutMetricPolicy: {} + + - EC2DescribePolicy: {} + + - DynamoDBCrudPolicy: + TableName: name + + - DynamoDBReadPolicy: + TableName: name + + - SESSendBouncePolicy: + IdentityName: name + + - ElasticsearchHttpPostPolicy: + DomainName: name + + - S3ReadPolicy: + BucketName: name + + - S3CrudPolicy: + BucketName: name + + - AMIDescribePolicy: {} + + - CloudFormationDescribeStacksPolicy: {} + + - RekognitionNoDataAccessPolicy: + CollectionId: id + + - RekognitionReadPolicy: + CollectionId: id + + - RekognitionWriteOnlyAccessPolicy: + CollectionId: id + + - SQSSendMessagePolicy: + QueueName: name + + - SNSPublishMessagePolicy: + TopicName: name + + - VPCAccessPolicy: {} + + - DynamoDBStreamReadPolicy: + TableName: name + StreamName: name + + - KinesisStreamReadPolicy: + StreamName: name + + - SESCrudPolicy: + IdentityName: name + + - SNSCrudPolicy: + TopicName: name + + - KinesisCrudPolicy: + StreamName: name + + - KMSDecryptPolicy: + KeyId: keyId + + - PollyFullAccessPolicy: + LexiconName: name + + - S3FullAccessPolicy: + BucketName: name + + - CodePipelineLambdaExecutionPolicy: {} + + - ServerlessRepoReadWriteAccessPolicy: {} + + - EC2CopyImagePolicy: + ImageId: id + + - CodePipelineReadOnlyPolicy: + PipelineName: pipeline + + - CloudWatchDashboardPolicy: {} + + - RekognitionFacesPolicy: + CollectionId: collection + + - RekognitionLabelsPolicy: {} + + - DynamoDBBackupFullAccessPolicy: + TableName: table + + - DynamoDBRestoreFromBackupPolicy: + TableName: table + + - ComprehendBasicAccessPolicy: {} + + - AWSSecretsManagerRotationPolicy: + FunctionName: function + + - MobileAnalyticsWriteOnlyAccessPolicy: {} + + - PinpointEndpointAccessPolicy: + PinpointApplicationId: id + + - RekognitionDetectOnlyPolicy: {} + + - RekognitionFacesManagementPolicy: + CollectionId: collection + + - EKSDescribePolicy: {} + + - CostExplorerReadOnlyPolicy: {} + + - OrganizationsListAccountsPolicy: {} + + - DynamoDBReconfigurePolicy: + TableName: name + + - SESBulkTemplatedCrudPolicy: + IdentityName: name + + - SESEmailTemplateCrudPolicy: {} + + - FilterLogEventsPolicy: + LogGroupName: name + + - SSMParameterReadPolicy: + ParameterName: name + + - StepFunctionsExecutionPolicy: + StateMachineName: name diff --git a/tests/functional/commands/validate/lib/models/api_cache.yaml b/tests/functional/commands/validate/lib/models/api_cache.yaml new file mode 100644 index 0000000000..2bd13be337 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_cache.yaml @@ -0,0 +1,22 @@ +Resources: + HtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + RestApiId: HtmlApi + Path: / + Method: get + + HtmlApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json + CacheClusterEnabled: true + CacheClusterSize: "1.6" \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_endpoint_configuration.yaml b/tests/functional/commands/validate/lib/models/api_endpoint_configuration.yaml new file mode 100644 index 0000000000..899d2d6925 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_endpoint_configuration.yaml @@ -0,0 +1,31 @@ +Parameters: + EndpointConfig: + Type: String + +Globals: + Api: + # Overriding this property for Implicit API + EndpointConfiguration: { + "Ref": "EndpointConfig" + } + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json + EndpointConfiguration: SomeValue diff --git a/tests/functional/commands/validate/lib/models/api_request_model.yaml b/tests/functional/commands/validate/lib/models/api_request_model.yaml new file mode 100644 index 0000000000..b7b8ed768a --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_request_model.yaml @@ -0,0 +1,28 @@ +Resources: + HtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + RestApiId: HtmlApi + Path: / + Method: get + RequestModel: + Model: User + Required: true + + HtmlApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + Models: + User: + type: object + properties: + username: + type: string diff --git a/tests/functional/commands/validate/lib/models/api_request_model_openapi_3.yaml b/tests/functional/commands/validate/lib/models/api_request_model_openapi_3.yaml new file mode 100644 index 0000000000..0394ca5153 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_request_model_openapi_3.yaml @@ -0,0 +1,42 @@ +Resources: + HtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + RestApiId: HtmlApi + Path: / + Method: get + RequestModel: + Model: User + Required: true + Iam: + Type: Api + Properties: + RequestModel: + Model: User + Required: true + RestApiId: + Ref: HtmlApi + Method: get + Path: /iam + Auth: + Authorizer: AWS_IAM + + + HtmlApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + OpenApiVersion: '3.0.1' + Models: + User: + type: object + properties: + username: + type: string \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_access_log_setting.yaml b/tests/functional/commands/validate/lib/models/api_with_access_log_setting.yaml new file mode 100644 index 0000000000..f6c1dd920a --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_access_log_setting.yaml @@ -0,0 +1,25 @@ +Globals: + Api: + AccessLogSetting: + DestinationArn: "arn:aws:logs:us-west-2:012345678901/API-Gateway-Execution-Logs_0123456789/prod:log-stream:12345678910" + Format: "$context.requestId" + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json diff --git a/tests/functional/commands/validate/lib/models/api_with_auth_all_maximum.yaml b/tests/functional/commands/validate/lib/models/api_with_auth_all_maximum.yaml new file mode 100644 index 0000000000..89c94fdc7e --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_auth_all_maximum.yaml @@ -0,0 +1,105 @@ +Resources: + MyApi: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: arn:aws:1 + Identity: + Header: MyAuthorizationHeader + ValidationExpression: myauthvalidationexpression + + MyCognitoAuthMultipleUserPools: + UserPoolArn: + - arn:aws:2 + - arn:aws:3 + Identity: + Header: MyAuthorizationHeader2 + ValidationExpression: myauthvalidationexpression2 + + MyLambdaTokenAuth: + FunctionPayloadType: TOKEN + FunctionArn: arn:aws + FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access + Identity: + Header: MyCustomAuthHeader + ValidationExpression: mycustomauthexpression + ReauthorizeEvery: 20 + + MyLambdaTokenAuthNoneFunctionInvokeRole: + FunctionArn: arn:aws + FunctionInvokeRole: NONE + Identity: + ReauthorizeEvery: 0 + + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: arn:aws + FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access + Identity: + Headers: + - Authorization1 + QueryStrings: + - Authorization2 + StageVariables: + - Authorization3 + Context: + - Authorization4 + ReauthorizeEvery: 0 + + MyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithNoAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: / + Method: get + Auth: + Authorizer: NONE + WithCognitoMultipleUserPoolsAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: post + Auth: + Authorizer: MyCognitoAuthMultipleUserPools + WithLambdaTokenAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: get + Auth: + Authorizer: MyLambdaTokenAuth + WithLambdaTokenAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: patch + Auth: + Authorizer: MyLambdaTokenAuthNoneFunctionInvokeRole + WithLambdaRequestAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: delete + Auth: + Authorizer: MyLambdaRequestAuth + WithDefaultAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: put \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_auth_all_maximum_openapi_3.yaml b/tests/functional/commands/validate/lib/models/api_with_auth_all_maximum_openapi_3.yaml new file mode 100644 index 0000000000..8cab3cd48c --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_auth_all_maximum_openapi_3.yaml @@ -0,0 +1,106 @@ +Resources: + MyApi: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + OpenApiVersion: '3.0.1' + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: arn:aws:1 + Identity: + Header: MyAuthorizationHeader + ValidationExpression: myauthvalidationexpression + + MyCognitoAuthMultipleUserPools: + UserPoolArn: + - arn:aws:2 + - arn:aws:3 + Identity: + Header: MyAuthorizationHeader2 + ValidationExpression: myauthvalidationexpression2 + + MyLambdaTokenAuth: + FunctionPayloadType: TOKEN + FunctionArn: arn:aws + FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access + Identity: + Header: MyCustomAuthHeader + ValidationExpression: mycustomauthexpression + ReauthorizeEvery: 20 + + MyLambdaTokenAuthNoneFunctionInvokeRole: + FunctionArn: arn:aws + FunctionInvokeRole: NONE + Identity: + ReauthorizeEvery: 0 + + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: arn:aws + FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access + Identity: + Headers: + - Authorization1 + QueryStrings: + - Authorization2 + StageVariables: + - Authorization3 + Context: + - Authorization4 + ReauthorizeEvery: 0 + + MyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithNoAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: / + Method: get + Auth: + Authorizer: NONE + WithCognitoMultipleUserPoolsAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: post + Auth: + Authorizer: MyCognitoAuthMultipleUserPools + WithLambdaTokenAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: get + Auth: + Authorizer: MyLambdaTokenAuth + WithLambdaTokenAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: patch + Auth: + Authorizer: MyLambdaTokenAuthNoneFunctionInvokeRole + WithLambdaRequestAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: delete + Auth: + Authorizer: MyLambdaRequestAuth + WithDefaultAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: put \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_auth_all_minimum.yaml b/tests/functional/commands/validate/lib/models/api_with_auth_all_minimum.yaml new file mode 100644 index 0000000000..75568f21dd --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_auth_all_minimum.yaml @@ -0,0 +1,78 @@ +Resources: + MyApiWithCognitoAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + + MyApiWithLambdaTokenAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyLambdaTokenAuth + Authorizers: + MyLambdaTokenAuth: + FunctionArn: !GetAtt MyAuthFn.Arn + + MyApiWithLambdaRequestAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyLambdaRequestAuth + Authorizers: + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: !GetAtt MyAuthFn.Arn + Identity: + Headers: + - Authorization1 + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + MyFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + Cognito: + Type: Api + Properties: + RestApiId: !Ref MyApiWithCognitoAuth + Method: get + Path: /cognito + LambdaToken: + Type: Api + Properties: + RestApiId: !Ref MyApiWithLambdaTokenAuth + Method: get + Path: /lambda-token + LambdaRequest: + Type: Api + Properties: + RestApiId: !Ref MyApiWithLambdaRequestAuth + Method: get + Path: /lambda-request + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_auth_all_minimum_openapi.yaml b/tests/functional/commands/validate/lib/models/api_with_auth_all_minimum_openapi.yaml new file mode 100644 index 0000000000..1ce331e2b0 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_auth_all_minimum_openapi.yaml @@ -0,0 +1,81 @@ +Globals: + Api: + OpenApiVersion: '3.0.1' +Resources: + MyApiWithCognitoAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + + MyApiWithLambdaTokenAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyLambdaTokenAuth + Authorizers: + MyLambdaTokenAuth: + FunctionArn: !GetAtt MyAuthFn.Arn + + MyApiWithLambdaRequestAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyLambdaRequestAuth + Authorizers: + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: !GetAtt MyAuthFn.Arn + Identity: + Headers: + - Authorization1 + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucketname/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + MyFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucketname/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + Cognito: + Type: Api + Properties: + RestApiId: !Ref MyApiWithCognitoAuth + Method: get + Path: /cognito + LambdaToken: + Type: Api + Properties: + RestApiId: !Ref MyApiWithLambdaTokenAuth + Method: get + Path: /lambda-token + LambdaRequest: + Type: Api + Properties: + RestApiId: !Ref MyApiWithLambdaRequestAuth + Method: get + Path: /lambda-request + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_auth_and_conditions_all_max.yaml b/tests/functional/commands/validate/lib/models/api_with_auth_and_conditions_all_max.yaml new file mode 100644 index 0000000000..f7c4a7db21 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_auth_and_conditions_all_max.yaml @@ -0,0 +1,119 @@ +Conditions: + PathCondition: + Fn::Equals: + - true + - true +Resources: + MyApi: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: arn:aws:1 + Identity: + Header: MyAuthorizationHeader + ValidationExpression: myauthvalidationexpression + + MyCognitoAuthMultipleUserPools: + UserPoolArn: + - arn:aws:2 + - arn:aws:3 + Identity: + Header: MyAuthorizationHeader2 + ValidationExpression: myauthvalidationexpression2 + + MyLambdaTokenAuth: + FunctionPayloadType: TOKEN + FunctionArn: arn:aws + FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access + Identity: + Header: MyCustomAuthHeader + ValidationExpression: mycustomauthexpression + ReauthorizeEvery: 20 + + MyLambdaTokenAuthNoneFunctionInvokeRole: + FunctionArn: arn:aws + FunctionInvokeRole: NONE + Identity: + ReauthorizeEvery: 0 + + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: arn:aws + FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access + Identity: + Headers: + - Authorization1 + QueryStrings: + - Authorization2 + StageVariables: + - Authorization3 + Context: + - Authorization4 + ReauthorizeEvery: 0 + + MyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithNoAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: / + Method: get + Auth: + Authorizer: NONE + WithCognitoMultipleUserPoolsAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: post + Auth: + Authorizer: MyCognitoAuthMultipleUserPools + WithLambdaTokenAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: get + Auth: + Authorizer: MyLambdaTokenAuth + + MyFunctionWithConditional: + Type: AWS::Serverless::Function + Condition: PathCondition + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithLambdaTokenAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: patch + Auth: + Authorizer: MyLambdaTokenAuthNoneFunctionInvokeRole + WithLambdaRequestAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: delete + Auth: + Authorizer: MyLambdaRequestAuth + WithDefaultAuthorizer: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: /users + Method: put \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_auth_no_default.yaml b/tests/functional/commands/validate/lib/models/api_with_auth_no_default.yaml new file mode 100644 index 0000000000..5b033d79ac --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_auth_no_default.yaml @@ -0,0 +1,75 @@ +Resources: + MyApiWithCognitoAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + + MyApiWithLambdaTokenAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + Authorizers: + MyLambdaTokenAuth: + FunctionArn: !GetAtt MyAuthFn.Arn + + MyApiWithLambdaRequestAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + Authorizers: + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: !GetAtt MyAuthFn.Arn + Identity: + Headers: + - Authorization1 + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + MyFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + Cognito: + Type: Api + Properties: + RestApiId: !Ref MyApiWithCognitoAuth + Method: get + Path: /cognito + LambdaToken: + Type: Api + Properties: + RestApiId: !Ref MyApiWithLambdaTokenAuth + Method: get + Path: /lambda-token + LambdaRequest: + Type: Api + Properties: + RestApiId: !Ref MyApiWithLambdaRequestAuth + Method: get + Path: /lambda-request + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_aws_iam_auth_overrides.yaml b/tests/functional/commands/validate/lib/models/api_with_aws_iam_auth_overrides.yaml new file mode 100644 index 0000000000..f9cd47765a --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_aws_iam_auth_overrides.yaml @@ -0,0 +1,86 @@ +Resources: + MyApiWithAwsIamAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: AWS_IAM + Authorizers: + MyCognitoAuth: + UserPoolArn: arn:aws:cognito-idp:xxxxxxxxx + InvokeRole: arn:aws:iam::123:role/AUTH_AWS_IAM + MyFunctionMyCognitoAuth: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + API1: + Type: Api + Properties: + RestApiId: !Ref MyApiWithAwsIamAuth + Method: get + Path: /MyFunctionMyCognitoAuth + Auth: + Authorizer: MyCognitoAuth + MyFunctionWithoutAuth: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + API2: + Type: Api + Properties: + RestApiId: !Ref MyApiWithAwsIamAuth + Method: get + Path: /MyFunctionWithoutAuth + MyFunctionNoneAuth: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + API3: + Type: Api + Properties: + RestApiId: !Ref MyApiWithAwsIamAuth + Method: get + Path: /MyFunctionNoneAuth + Auth: + Authorizer: NONE + MyFunctionDefaultInvokeRole: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + API3: + Type: Api + Properties: + RestApiId: !Ref MyApiWithAwsIamAuth + Method: get + Path: /MyFunctionDefaultInvokeRole + Auth: + Authorizer: AWS_IAM + InvokeRole: CALLER_CREDENTIALS + MyFunctionCustomInvokeRole: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + API3: + Type: Api + Properties: + RestApiId: !Ref MyApiWithAwsIamAuth + Method: get + Path: /MyFunctionCustomInvokeRole + Auth: + Authorizer: AWS_IAM + InvokeRole: arn:aws:iam::456::role/something-else diff --git a/tests/functional/commands/validate/lib/models/api_with_binary_media_types.yaml b/tests/functional/commands/validate/lib/models/api_with_binary_media_types.yaml new file mode 100644 index 0000000000..dfe4ed0f22 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_binary_media_types.yaml @@ -0,0 +1,32 @@ +Parameters: + BMT: + Type: String + Default: 'image~1jpg' +Globals: + Api: + BinaryMediaTypes: + - image~1gif + - {"Fn::Join": ["~1", ["image", "png"]]} + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json + BinaryMediaTypes: + - application~1octet-stream + - !Ref BMT diff --git a/tests/functional/commands/validate/lib/models/api_with_binary_media_types_definition_body.yaml b/tests/functional/commands/validate/lib/models/api_with_binary_media_types_definition_body.yaml new file mode 100644 index 0000000000..6730419d4f --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_binary_media_types_definition_body.yaml @@ -0,0 +1,29 @@ +Parameters: + BMT: + Type: String + Default: image~1jpeg +Globals: + Api: + BinaryMediaTypes: + - !Ref BMT + - image~1jpg + - {"Fn::Join": ["~1", ["image", "png"]]} + +Resources: + ExplicitApiManagedSwagger: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + BinaryMediaTypes: + - image~1gif + + ExplicitApiDefinitionBody: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + BinaryMediaTypes: + - application~1json + DefinitionBody: { + "paths": {}, + "swagger": "2.0", + } diff --git a/tests/functional/commands/validate/lib/models/api_with_canary_setting.yaml b/tests/functional/commands/validate/lib/models/api_with_canary_setting.yaml new file mode 100644 index 0000000000..d6fd057721 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_canary_setting.yaml @@ -0,0 +1,28 @@ +Globals: + Api: + CanarySetting: + PercentTraffic: 100 + StageVariablesOverrides: + sv1: "test" + sv2: "test2" + UseStageCache: false + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json diff --git a/tests/functional/commands/validate/lib/models/api_with_cors.yaml b/tests/functional/commands/validate/lib/models/api_with_cors.yaml new file mode 100644 index 0000000000..6c6765dba8 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors.yaml @@ -0,0 +1,80 @@ +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + AnyApi: + Type: Api + Properties: + Path: /foo + Method: any + RestApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs8.10 + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs8.10 + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/add": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } + Cors: + AllowMethods: "methods" + AllowHeaders: "headers" + AllowOrigin: "origins" + AllowCredentials: true diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_and_auth_no_preflight_auth.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_and_auth_no_preflight_auth.yaml new file mode 100644 index 0000000000..e33f486bad --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_and_auth_no_preflight_auth.yaml @@ -0,0 +1,48 @@ +Globals: + Api: + Cors: "origins" + +Resources: + ApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + RestApiId: !Ref ServerlessApi + + PostHtml: + Type: Api + Properties: + Path: / + Method: post + RestApiId: !Ref ServerlessApi + + + ServerlessApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + Auth: + AddDefaultAuthorizerToCorsPreflight: False + DefaultAuthorizer: MyLambdaRequestAuth + Authorizers: + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: !GetAtt MyAuthFn.Arn + Identity: + Headers: + - Authorization1 + + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_and_auth_preflight_auth.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_and_auth_preflight_auth.yaml new file mode 100644 index 0000000000..45032209ce --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_and_auth_preflight_auth.yaml @@ -0,0 +1,47 @@ +Globals: + Api: + Cors: "origins" + +Resources: + ApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + RestApiId: !Ref ServerlessApi + + PostHtml: + Type: Api + Properties: + Path: / + Method: post + RestApiId: !Ref ServerlessApi + + + ServerlessApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyLambdaRequestAuth + Authorizers: + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: !GetAtt MyAuthFn.Arn + Identity: + Headers: + - Authorization1 + + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_and_conditions_no_definitionbody.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_and_conditions_no_definitionbody.yaml new file mode 100644 index 0000000000..67129b0e8d --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_and_conditions_no_definitionbody.yaml @@ -0,0 +1,55 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Conditions: + MyCondition: + Fn::Equals: + - true + - true +Globals: + Api: + # If we skip AllowMethods, then SAM will auto generate a list of methods scoped to each path + Cors: + AllowOrigin: "'www.example.com'" + +Resources: + ImplicitApiFunction2: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs8.10 + Events: + DeleteHtml: + Type: Api + Properties: + RestApiId: !Ref ExplicitApi + Path: / + Method: delete + + ImplicitApiFunction: + Type: AWS::Serverless::Function + Condition: MyCondition + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs8.10 + Events: + GetHtml: + Type: Api + Properties: + RestApiId: !Ref ExplicitApi + Path: / + Method: get + + PostHtml: + Type: Api + Properties: + RestApiId: !Ref ExplicitApi + Path: / + Method: post + + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_and_only_credentials_false.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_credentials_false.yaml new file mode 100644 index 0000000000..37f9e772d2 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_credentials_false.yaml @@ -0,0 +1,54 @@ +Globals: + Api: + Cors: + AllowCredentials: false + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs8.10 + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/add": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } + + + diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_and_only_headers.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_headers.yaml new file mode 100644 index 0000000000..6f1a9da497 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_headers.yaml @@ -0,0 +1,70 @@ +Globals: + Api: + Cors: + + # If we skip AllowMethods, then SAM will auto generate a list of methods scoped to each path + AllowHeaders: "headers" + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + PostHtml: + Type: Api + Properties: + Path: / + Method: post + + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/add": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } + + + diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_and_only_maxage.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_maxage.yaml new file mode 100644 index 0000000000..62ade857bd --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_maxage.yaml @@ -0,0 +1,55 @@ +Globals: + Api: + Cors: + # Minutes + MaxAge: 600 + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs8.10 + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/add": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } + + + diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_and_only_methods.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_methods.yaml new file mode 100644 index 0000000000..41b51515c9 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_methods.yaml @@ -0,0 +1,19 @@ +Globals: + Api: + Cors: + AllowMethods: "methods" + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_and_only_origins.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_origins.yaml new file mode 100644 index 0000000000..2fab3d122d --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_and_only_origins.yaml @@ -0,0 +1,68 @@ +Globals: + Api: + # If we skip AllowMethods, then SAM will auto generate a list of methods scoped to each path + Cors: "origins" + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + PostHtml: + Type: Api + Properties: + Path: / + Method: post + + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/add": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } + + + diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_no_definitionbody.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_no_definitionbody.yaml new file mode 100644 index 0000000000..465422530f --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_no_definitionbody.yaml @@ -0,0 +1,34 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Globals: + Api: + # If we skip AllowMethods, then SAM will auto generate a list of methods scoped to each path + Cors: "origins" + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + RestApiId: !Ref ExplicitApi + Path: / + Method: get + + PostHtml: + Type: Api + Properties: + RestApiId: !Ref ExplicitApi + Path: / + Method: post + + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_cors_openapi_3.yaml b/tests/functional/commands/validate/lib/models/api_with_cors_openapi_3.yaml new file mode 100644 index 0000000000..7465b9ee76 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_cors_openapi_3.yaml @@ -0,0 +1,82 @@ +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + OpenApiVersion: '3.0' + + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + AnyApi: + Type: Api + Properties: + Path: /foo + Method: any + RestApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs8.10 + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs8.10 + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/add": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } + Cors: + AllowMethods: "methods" + AllowHeaders: "headers" + AllowOrigin: "origins" + AllowCredentials: true diff --git a/tests/functional/commands/validate/lib/models/api_with_default_aws_iam_auth.yaml b/tests/functional/commands/validate/lib/models/api_with_default_aws_iam_auth.yaml new file mode 100644 index 0000000000..1ab0f6600f --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_default_aws_iam_auth.yaml @@ -0,0 +1,47 @@ +Resources: + MyApiWithAwsIamAuthAndDefaultInvokeRole: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: AWS_IAM + InvokeRole: CALLER_CREDENTIALS + MyApiWithAwsIamAuthAndCustomInvokeRole: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: AWS_IAM + InvokeRole: rn:aws:iam::123:role/AUTH_AWS_IAM + MyApiWithAwsIamAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: AWS_IAM + + MyFunctionWithAwsIamAuth: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + MyApiWithAwsIamAuth: + Type: Api + Properties: + RestApiId: !Ref MyApiWithAwsIamAuth + Path: / + Method: get + MyApiWithAwsIamAuthAndCustomInvokeRole: + Type: Api + Properties: + RestApiId: !Ref MyApiWithAwsIamAuthAndCustomInvokeRole + Path: / + Method: post + MyApiWithAwsIamAuthAndDefaultInvokeRole: + Type: Api + Properties: + RestApiId: !Ref MyApiWithAwsIamAuthAndDefaultInvokeRole + Path: / + Method: put diff --git a/tests/functional/commands/validate/lib/models/api_with_gateway_responses.yaml b/tests/functional/commands/validate/lib/models/api_with_gateway_responses.yaml new file mode 100644 index 0000000000..1bb772aa93 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_gateway_responses.yaml @@ -0,0 +1,28 @@ +Resources: + Function: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + RestApiId: !Ref ExplicitApi + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + GatewayResponses: + UNAUTHORIZED: + StatusCode: 401 + ResponseParameters: + Headers: + Access-Control-Expose-Headers: "'WWW-Authenticate'" + Access-Control-Allow-Origin: "'*'" + WWW-Authenticate: >- + 'Bearer realm="admin"' diff --git a/tests/functional/commands/validate/lib/models/api_with_gateway_responses_all.yaml b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_all.yaml new file mode 100644 index 0000000000..4a7bdfec86 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_all.yaml @@ -0,0 +1,37 @@ +Resources: + Function: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + RestApiId: !Ref ExplicitApi + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + GatewayResponses: + UNAUTHORIZED: + StatusCode: 401 + ResponseParameters: + Headers: + Access-Control-Expose-Headers: "'WWW-Authenticate'" + Access-Control-Allow-Origin: "'*'" + WWW-Authenticate: >- + 'Bearer realm="admin"' + Paths: + PathKey: "'path-value'" + QueryStrings: + QueryStringKey: "'query-string-value'" + QUOTA_EXCEEDED: + StatusCode: 429 + ResponseParameters: + Headers: + Retry-After: "'31536000'" diff --git a/tests/functional/commands/validate/lib/models/api_with_gateway_responses_all_openapi_3.yaml b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_all_openapi_3.yaml new file mode 100644 index 0000000000..fe88f63d54 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_all_openapi_3.yaml @@ -0,0 +1,38 @@ +Resources: + Function: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + RestApiId: !Ref ExplicitApi + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + OpenApiVersion: '3.0' + GatewayResponses: + UNAUTHORIZED: + StatusCode: 401 + ResponseParameters: + Headers: + Access-Control-Expose-Headers: "'WWW-Authenticate'" + Access-Control-Allow-Origin: "'*'" + WWW-Authenticate: >- + 'Bearer realm="admin"' + Paths: + PathKey: "'path-value'" + QueryStrings: + QueryStringKey: "'query-string-value'" + QUOTA_EXCEEDED: + StatusCode: 429 + ResponseParameters: + Headers: + Retry-After: "'31536000'" diff --git a/tests/functional/commands/validate/lib/models/api_with_gateway_responses_implicit.yaml b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_implicit.yaml new file mode 100644 index 0000000000..1bd7540688 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_implicit.yaml @@ -0,0 +1,27 @@ +Globals: + Api: + Name: "some api" + GatewayResponses: + UNAUTHORIZED: + StatusCode: 401 + ResponseParameters: + Headers: + Access-Control-Expose-Headers: "'WWW-Authenticate'" + Access-Control-Allow-Origin: "'*'" + WWW-Authenticate: >- + 'Bearer realm="admin"' + + +Resources: + Function: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_gateway_responses_minimal.yaml b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_minimal.yaml new file mode 100644 index 0000000000..e74053bd72 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_minimal.yaml @@ -0,0 +1,21 @@ +Resources: + Function: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + RestApiId: !Ref ExplicitApi + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + GatewayResponses: + UNAUTHORIZED: {} diff --git a/tests/functional/commands/validate/lib/models/api_with_gateway_responses_string_status_code.yaml b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_string_status_code.yaml new file mode 100644 index 0000000000..22d807c7c3 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_gateway_responses_string_status_code.yaml @@ -0,0 +1,28 @@ +Resources: + Function: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + RestApiId: !Ref ExplicitApi + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + GatewayResponses: + UNAUTHORIZED: + StatusCode: '401' + ResponseParameters: + Headers: + Access-Control-Expose-Headers: "'WWW-Authenticate'" + Access-Control-Allow-Origin: "'*'" + WWW-Authenticate: >- + 'Bearer realm="admin"' diff --git a/tests/functional/commands/validate/lib/models/api_with_method_aws_iam_auth.yaml b/tests/functional/commands/validate/lib/models/api_with_method_aws_iam_auth.yaml new file mode 100644 index 0000000000..4becdb90a0 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_method_aws_iam_auth.yaml @@ -0,0 +1,39 @@ +Resources: + MyApiWithoutAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + + MyFunctionWithAwsIamAuth: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs8.10 + Events: + MyApiWithAwsIamAuth: + Type: Api + Properties: + RestApiId: !Ref MyApiWithoutAuth + Path: / + Method: get + Auth: + Authorizer: AWS_IAM + MyApiWithAwsIamAuthAndCustomInvokeRole: + Type: Api + Properties: + RestApiId: !Ref MyApiWithoutAuth + Path: / + Method: post + Auth: + Authorizer: AWS_IAM + InvokeRole: rn:aws:iam::123:role/AUTH_AWS_IAM + MyApiWithAwsIamAuthAndDefaultInvokeRole: + Type: Api + Properties: + RestApiId: !Ref MyApiWithoutAuth + Path: / + Method: put + Auth: + Authorizer: AWS_IAM + InvokeRole: CALLER_CREDENTIALS diff --git a/tests/functional/commands/validate/lib/models/api_with_method_settings.yaml b/tests/functional/commands/validate/lib/models/api_with_method_settings.yaml new file mode 100644 index 0000000000..efccaee35b --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_method_settings.yaml @@ -0,0 +1,36 @@ +Globals: + Api: + MethodSettings: [{ + # LOGGING!! + "LoggingLevel": "INFO", + + # METRICS!! + "MetricsEnabled": True, + + # Trace-level Logging + "DataTraceEnabled": True, + + # On all Paths & methods + "ResourcePath": "/*", + "HttpMethod": "*", + }] + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json diff --git a/tests/functional/commands/validate/lib/models/api_with_minimum_compression_size.yaml b/tests/functional/commands/validate/lib/models/api_with_minimum_compression_size.yaml new file mode 100644 index 0000000000..79191d87ad --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_minimum_compression_size.yaml @@ -0,0 +1,24 @@ +Globals: + Api: + MinimumCompressionSize: 1024 + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json + MinimumCompressionSize: 256 diff --git a/tests/functional/commands/validate/lib/models/api_with_open_api_version.yaml b/tests/functional/commands/validate/lib/models/api_with_open_api_version.yaml new file mode 100644 index 0000000000..dd91b26c9c --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_open_api_version.yaml @@ -0,0 +1,22 @@ +Globals: + Api: + OpenApiVersion: '3.0.1' + Cors: '*' + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs8.10 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod diff --git a/tests/functional/commands/validate/lib/models/api_with_open_api_version_2.yaml b/tests/functional/commands/validate/lib/models/api_with_open_api_version_2.yaml new file mode 100644 index 0000000000..b31fc72b93 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_open_api_version_2.yaml @@ -0,0 +1,22 @@ +Globals: + Api: + OpenApiVersion: '2.0' + Cors: '*' + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs8.10 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod diff --git a/tests/functional/commands/validate/lib/models/api_with_openapi_definition_body_no_flag.yaml b/tests/functional/commands/validate/lib/models/api_with_openapi_definition_body_no_flag.yaml new file mode 100644 index 0000000000..9d0749660e --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_openapi_definition_body_no_flag.yaml @@ -0,0 +1,58 @@ +Globals: + Api: + Name: "some api" + CacheClusterEnabled: True + CacheClusterSize: "1.6" + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + Variables: + SomeVar: Value + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: SomeStage + DefinitionBody: + openapi: 3.1.1 + info: + version: '1.0' + title: !Ref AWS::StackName + paths: + "/": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations + responses: {} + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_resource_refs.yaml b/tests/functional/commands/validate/lib/models/api_with_resource_refs.yaml new file mode 100644 index 0000000000..3381677ef2 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_resource_refs.yaml @@ -0,0 +1,34 @@ +# Test if resource references work for both Explicit API & Implicit API resources + +Resources: + MyApi: + Type: 'AWS::Serverless::Api' + Properties: + StageName: foo + DefinitionBody: + "this": "is" + "a": "swagger" + + MyFunction: + Type: "AWS::Serverless::Function" + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Events: + GetHtml: + Type: Api + Properties: + Path: /html + Method: GET + +Outputs: + ImplicitApiDeployment: + Value: !Ref ServerlessRestApi.Deployment + ImplicitApiStage: + Value: !Ref ServerlessRestApi.Stage + ExplicitApiDeployment: + Value: !Ref MyApi.Deployment + ExplicitApiStage: + Value: !Ref MyApi.Stage + diff --git a/tests/functional/commands/validate/lib/models/api_with_swagger_and_openapi_with_auth.yaml b/tests/functional/commands/validate/lib/models/api_with_swagger_and_openapi_with_auth.yaml new file mode 100644 index 0000000000..0e8ee19e74 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_swagger_and_openapi_with_auth.yaml @@ -0,0 +1,59 @@ +Globals: + Api: + Name: "some api" + CacheClusterEnabled: True + CacheClusterSize: "1.6" + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + Variables: + SomeVar: Value + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: SomeStage + DefinitionBody: + openapi: 3.1.1 + swagger: 2.0 + info: + version: '1.0' + title: !Ref AWS::StackName + paths: + "/": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations + responses: {} + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/api_with_xray_tracing.yaml b/tests/functional/commands/validate/lib/models/api_with_xray_tracing.yaml new file mode 100644 index 0000000000..2752463db6 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/api_with_xray_tracing.yaml @@ -0,0 +1,21 @@ +Resources: + HtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + RestApiId: HtmlApi + Path: / + Method: get + + HtmlApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json + TracingEnabled: true diff --git a/tests/functional/commands/validate/lib/models/basic_function.yaml b/tests/functional/commands/validate/lib/models/basic_function.yaml new file mode 100644 index 0000000000..ae6ba50b4f --- /dev/null +++ b/tests/functional/commands/validate/lib/models/basic_function.yaml @@ -0,0 +1,115 @@ +Parameters: + SomeParameter: + Type: String + Default: param + SomeOtherParameter: + Type: String + Default: otherparam +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + FunctionWithTracing: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Tracing: Active + + FunctionWithInlineCode: + Type: 'AWS::Serverless::Function' + Properties: + InlineCode: "hello world" + Handler: hello.handler + Runtime: python2.7 + Tracing: Active + + FunctionWithCodeUriObject: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: + Bucket: somebucket + Key: somekey + Version: "1" + Handler: hello.handler + Runtime: python2.7 + + CompleteFunction: + Type: 'AWS::Serverless::Function' + Properties: + FunctionName: MyAwesomeFunction + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Description: Starter Lambda Function + Timeout: 60 + VpcConfig: + SecurityGroupIds: + - sg-edcd9784 + SubnetIds: + - subnet-9d4a7b6c + - subnet-65ea5f08 + - {Ref: SomeParameter} + - {Ref: SomeOtherParameter} + Role: arn:aws:iam::012345678901:role/lambda_basic_execution + Environment: + Variables: + Name: Value + Name2: Value2 + FunctionWithPolicies: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Policies: AmazonDynamoDBFullAccess + FunctionWithPolicyDocument: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Policies: + Statement: + - Action: [ 'dynamodb:*' ] + Effect: Allow + Resource: '*' + FunctionWithRoleRef: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Role: + Fn::GetAtt: ["MyFunctionRole", "Arn"] + + MyFunctionRole: + # This is just some role. Actual role definition might be wrong + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - + Effect: "Allow" + Principal: + Service: + - "ec2.amazonaws.com" + Action: + - "sts:AssumeRole" + Path: "/" + Policies: + - + PolicyName: "root" + PolicyDocument: + Version: "2012-10-17" + Statement: + - + Effect: "Allow" + Action: "*" + Resource: "*" diff --git a/tests/functional/commands/validate/lib/models/basic_function_with_tags.yaml b/tests/functional/commands/validate/lib/models/basic_function_with_tags.yaml new file mode 100644 index 0000000000..aced4f2d37 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/basic_function_with_tags.yaml @@ -0,0 +1,28 @@ +# File: sam.yml +# Version: 0.9 + +AWSTemplateFormatVersion: '2010-09-09' +Parameters: + TagValueParam: + Type: String + Default: Val +Resources: + AlexaSkillFunc: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Description: Created by SAM + Events: + AlexaSkillEvent: + Type: AlexaSkill + Handler: index.handler + MemorySize: 1024 + Runtime: nodejs4.3 + Timeout: 3 + Tags: + TagKey1: TagValue1 + TagKey2: "" + TagKey3: + Ref: TagValueParam + TagKey4: "123" + diff --git a/tests/functional/commands/validate/lib/models/basic_layer.yaml b/tests/functional/commands/validate/lib/models/basic_layer.yaml new file mode 100644 index 0000000000..170af09b1a --- /dev/null +++ b/tests/functional/commands/validate/lib/models/basic_layer.yaml @@ -0,0 +1,38 @@ +Conditions: + TestCondition: + Fn::Equals: + - beta + - beta + +Resources: + MinimalLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + + LayerWithContentUriObject: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: + Bucket: somebucket + Key: somekey + Version: "v1" + RetentionPolicy: Delete + + CompleteLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + LayerName: MyAwesomeLayer + ContentUri: s3://sam-demo-bucket/layer.zip + Description: Starter Lambda Layer + CompatibleRuntimes: + - python3.6 + - python2.7 + LicenseInfo: "License information" + RetentionPolicy: Retain + + LayerWithCondition: + Type: 'AWS::Serverless::LayerVersion' + Condition: TestCondition + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip diff --git a/tests/functional/commands/validate/lib/models/cloudwatch_logs_with_ref.yaml b/tests/functional/commands/validate/lib/models/cloudwatch_logs_with_ref.yaml new file mode 100644 index 0000000000..b818b66d8b --- /dev/null +++ b/tests/functional/commands/validate/lib/models/cloudwatch_logs_with_ref.yaml @@ -0,0 +1,25 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example CloudWatch Logs + Lambda +Parameters: + LogGroupName: + Type: String + Default: MyCWLogGroup + FilterPattern: + Type: String + Default: My filter pattern + +Resources: + TriggeredFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip?versionId=3Tcgv52_0GaDvhDva4YciYeqRyPnpIcO + Handler: hello.handler + Runtime: python2.7 + Events: + CWLogEvent: + Type: CloudWatchLogs + Properties: + LogGroupName: !Ref LogGroupName + FilterPattern: !Ref FilterPattern + \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/cloudwatchevent.yaml b/tests/functional/commands/validate/lib/models/cloudwatchevent.yaml new file mode 100644 index 0000000000..28db31f84d --- /dev/null +++ b/tests/functional/commands/validate/lib/models/cloudwatchevent.yaml @@ -0,0 +1,26 @@ +Resources: + ScheduledFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip?versionId=3Tcgv52_0GaDvhDva4YciYeqRyPnpIcO + Handler: hello.handler + Runtime: python2.7 + Events: + Schedule: + Type: Schedule + Properties: + Schedule: 'rate(1 minute)' + TriggeredFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip?versionId=3Tcgv52_0GaDvhDva4YciYeqRyPnpIcO + Handler: hello.handler + Runtime: python2.7 + Events: + OnTerminate: + Type: CloudWatchEvent + Properties: + Pattern: + detail: + state: + - terminated diff --git a/tests/functional/commands/validate/lib/models/cloudwatchlog.yaml b/tests/functional/commands/validate/lib/models/cloudwatchlog.yaml new file mode 100644 index 0000000000..42b2f77151 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/cloudwatchlog.yaml @@ -0,0 +1,14 @@ +Resources: + TriggeredFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip?versionId=3Tcgv52_0GaDvhDva4YciYeqRyPnpIcO + Handler: hello.handler + Runtime: python2.7 + Events: + CWLogEvent: + Type: CloudWatchLogs + Properties: + LogGroupName: MyCWLogGroup + FilterPattern: My filter pattern + diff --git a/tests/functional/commands/validate/lib/models/depends_on.yaml b/tests/functional/commands/validate/lib/models/depends_on.yaml new file mode 100644 index 0000000000..7ea5e5aad9 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/depends_on.yaml @@ -0,0 +1,48 @@ +# SAM template containing DependsOn property on resources. Output resources should +# also have this property set +Resources: + MyFunction: + Type: 'AWS::Serverless::Function' + DependsOn: ["MyExplicitApi", "MySamTable"] + Properties: + CodeUri: s3://sam-demo-bucket/code.zip + Handler: index.handler + Runtime: nodejs4.3 + Events: + MyApi: + Type: Api + Properties: + Path: / + Method: GET + RestApiId: MyExplicitApi + + MyExplicitApi: + Type: AWS::Serverless::Api + DependsOn: "MySamTable" + Properties: + DefinitionUri: s3://sam-translator-tests-dont-delete/swagger-http.json + StageName: dev + + + MySamTable: + Type: AWS::Serverless::SimpleTable + + + MyOtherTable: + # Test DependsOn property a non-SAM resource + Type: AWS::DynamoDB::Table + DependsOn: "MySamTable" + Properties: + + AttributeDefinitions: + - { AttributeName : id, AttributeType : S } + + KeySchema: + - { "AttributeName" : "id", "KeyType" : "HASH"} + + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + + StreamSpecification: + StreamViewType: "NEW_IMAGE" diff --git a/tests/functional/commands/validate/lib/models/explicit_api.yaml b/tests/functional/commands/validate/lib/models/explicit_api.yaml new file mode 100644 index 0000000000..0cef803f77 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/explicit_api.yaml @@ -0,0 +1,49 @@ +Parameters: + MyStageName: + Type: String + Default: Production + something: + Type: String + Default: something + +Resources: + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: + Bucket: sam-demo-bucket + Key: webpage.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Policies: AmazonDynamoDBReadOnlyAccess + Events: + GetHtml: + Type: Api + Properties: + RestApiId: + Ref: GetHtmlApi + Path: / + Method: get + + GetHtmlApi: + Type: AWS::Serverless::Api + Properties: + Name: MyGetApi + StageName: + Ref: MyStageName + DefinitionUri: + Bucket: sam-demo-bucket + Key: webpage_swagger.json + Variables: + EndpointUri: + Ref: something + EndpointUri2: http://example.com + + ApiWithInlineSwagger: + Type: AWS::Serverless::Api + Properties: + StageName: + Ref: MyStageName + DefinitionBody: + "this": "is" + "a": "inline swagger" diff --git a/tests/functional/commands/validate/lib/models/explicit_api_openapi_3.yaml b/tests/functional/commands/validate/lib/models/explicit_api_openapi_3.yaml new file mode 100644 index 0000000000..2a1bc936d1 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/explicit_api_openapi_3.yaml @@ -0,0 +1,50 @@ +Parameters: + MyStageName: + Type: String + Default: Production + something: + Type: String + Default: something + +Resources: + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: + Bucket: sam-demo-bucket + Key: webpage.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Policies: AmazonDynamoDBReadOnlyAccess + Events: + GetHtml: + Type: Api + Properties: + RestApiId: + Ref: GetHtmlApi + Path: / + Method: get + + GetHtmlApi: + Type: AWS::Serverless::Api + Properties: + Name: MyGetApi + StageName: + Ref: MyStageName + DefinitionUri: + Bucket: sam-demo-bucket + Key: webpage_swagger.json + Variables: + EndpointUri: + Ref: something + EndpointUri2: http://example.com + + ApiWithInlineSwagger: + Type: AWS::Serverless::Api + Properties: + StageName: + Ref: MyStageName + OpenApiVersion: '3.0' + DefinitionBody: + "this": "is" + "a": "inline swagger" diff --git a/tests/functional/commands/validate/lib/models/explicit_api_with_invalid_events_config.yaml b/tests/functional/commands/validate/lib/models/explicit_api_with_invalid_events_config.yaml new file mode 100644 index 0000000000..9f0e4285b5 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/explicit_api_with_invalid_events_config.yaml @@ -0,0 +1,51 @@ +# This is specifically testing a invalid SAM template, that is currently accepted by SAM, and some customers rely on this behavior. +# We will eventually change the behavior to error on this invalid template, but until then, this test will guard against +# inadvertently changing this behavior. + +# When a Function's Event contains a path that is "not" in the Swagger, the behavior would be just add Lambda::Permissions +# and leaving the Swagger unmodified. + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.restapi + Runtime: nodejs4.3 + Events: + AddApi: + Type: Api + Properties: + # /add is NOT present in the Swagger. + Path: /add + Method: post + RestApiId: ApiWithInlineSwagger + + + ApiWithInlineSwagger: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/foo": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } diff --git a/tests/functional/commands/validate/lib/models/function_concurrency.yaml b/tests/functional/commands/validate/lib/models/function_concurrency.yaml new file mode 100644 index 0000000000..ff39277a4e --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_concurrency.yaml @@ -0,0 +1,22 @@ +Parameters: + Concurrency: + Type: Number + +Resources: + ConcurrentFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + ReservedConcurrentExecutions: 100 + + AnotherFunction: + Type: "AWS::Serverless::Function" + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + ReservedConcurrentExecutions: { + "Ref": "Concurrency" + } \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/function_event_conditions.yaml b/tests/functional/commands/validate/lib/models/function_event_conditions.yaml new file mode 100644 index 0000000000..3dd9bad2a4 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_event_conditions.yaml @@ -0,0 +1,93 @@ +Conditions: + MyCondition: + Fn::Equals: + - true + - true + +Resources: + +# S3 Event without condition, using same bucket + FunctionOne: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + +# All Event Types + MyAwesomeFunction: + Type: 'AWS::Serverless::Function' + Condition: MyCondition + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + AutoPublishAlias: Live + + Events: + CWSchedule: + Type: Schedule + Properties: + Schedule: 'rate(1 minute)' + + CWEvent: + Type: CloudWatchEvent + Properties: + Pattern: + detail: + state: + - terminated + + CWLog: + Type: CloudWatchLogs + Properties: + LogGroupName: MyLogGroup + FilterPattern: My pattern + + IoTRule: + Type: IoTRule + Properties: + Sql: SELECT * FROM 'topic/test' + AwsIotSqlVersion: beta + + S3Trigger: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + NotificationTopic: + Type: SNS + Properties: + Topic: + Ref: Notifications + + KinesisStream: + Type: Kinesis + Properties: + Stream: arn:aws:kinesis:us-west-2:012345678901:stream/my-stream + BatchSize: 100 + StartingPosition: TRIM_HORIZON + + DDBStream: + Type: DynamoDB + Properties: + Stream: arn:aws:dynamodb:us-west-2:012345678901:table/TestTable/stream/2015-05-11T21:21:33.291 + BatchSize: 200 + StartingPosition: LATEST + + Notifications: + Condition: MyCondition + Type: AWS::SNS::Topic + + Images: + Type: AWS::S3::Bucket diff --git a/tests/functional/commands/validate/lib/models/function_managed_inline_policy.yaml b/tests/functional/commands/validate/lib/models/function_managed_inline_policy.yaml new file mode 100644 index 0000000000..538d5f3c69 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_managed_inline_policy.yaml @@ -0,0 +1,26 @@ +Parameters: + SomeManagedPolicyArn: + Type: String + Default: arn:aws:iam::aws:policy/OtherPolicy +Resources: + Function: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Policies: + - Statement: + - Action: [ 'dynamodb:*' ] + Effect: Allow + Resource: '*' + - AmazonDynamoDBFullAccess + # Duplicate Policies should get de-duped + - AmazonDynamoDBFullAccess + - AWSLambdaBasicExecutionRole + + - AWSLambdaRole + + # Intrinsic functions & custom policy ARNs must be supported + - {"Ref": "SomeManagedPolicyArn"} + - arn:aws:iam::123456789012:policy/CustomerCreatedManagedPolicy diff --git a/tests/functional/commands/validate/lib/models/function_with_alias.yaml b/tests/functional/commands/validate/lib/models/function_with_alias.yaml new file mode 100644 index 0000000000..4a2defb45e --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_alias.yaml @@ -0,0 +1,10 @@ +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + VersionDescription: sam-testing + diff --git a/tests/functional/commands/validate/lib/models/function_with_alias_and_event_sources.yaml b/tests/functional/commands/validate/lib/models/function_with_alias_and_event_sources.yaml new file mode 100644 index 0000000000..0d436671c7 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_alias_and_event_sources.yaml @@ -0,0 +1,102 @@ +# Testing Alias Invoke with ALL event sources supported by Lambda +# We are looking to check if the event sources and their associated Lambda::Permission resources are +# connect to the Alias and *not* the function +Parameters: + MyStageName: + Type: String + Default: beta +Resources: + MyAwesomeFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + AutoPublishAlias: Live + + Events: + CWSchedule: + Type: Schedule + Properties: + Schedule: 'rate(1 minute)' + + CWEvent: + Type: CloudWatchEvent + Properties: + Pattern: + detail: + state: + - terminated + + CWLog: + Type: CloudWatchLogs + Properties: + LogGroupName: MyLogGroup + FilterPattern: My pattern + + ExplicitApi: + Type: Api + Properties: + RestApiId: + Ref: GetHtmlApi + Path: / + Method: get + + ImplicitApi: + Type: Api + Properties: + Path: /add + Method: post + + IoTRule: + Type: IoTRule + Properties: + Sql: SELECT * FROM 'topic/test' + AwsIotSqlVersion: beta + + S3Trigger: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + NotificationTopic: + Type: SNS + Properties: + Topic: + Ref: Notifications + + KinesisStream: + Type: Kinesis + Properties: + Stream: arn:aws:kinesis:us-west-2:012345678901:stream/my-stream + BatchSize: 100 + StartingPosition: TRIM_HORIZON + + DDBStream: + Type: DynamoDB + Properties: + Stream: arn:aws:dynamodb:us-west-2:012345678901:table/TestTable/stream/2015-05-11T21:21:33.291 + BatchSize: 200 + StartingPosition: LATEST + + Notifications: + Type: AWS::SNS::Topic + + Images: + Type: AWS::S3::Bucket + + GetHtmlApi: + Type: AWS::Serverless::Api + Properties: + Name: MyGetApi + StageName: + Ref: MyStageName + DefinitionUri: + Bucket: sam-demo-bucket + Key: webpage_swagger.json + Variables: + LambdaFunction: + Ref: "MyAwesomeFunction" diff --git a/tests/functional/commands/validate/lib/models/function_with_alias_intrinsics.yaml b/tests/functional/commands/validate/lib/models/function_with_alias_intrinsics.yaml new file mode 100644 index 0000000000..99327286bf --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_alias_intrinsics.yaml @@ -0,0 +1,15 @@ +Parameters: + AliasName: + Type: String + Default: live + +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: + Ref: AliasName + diff --git a/tests/functional/commands/validate/lib/models/function_with_condition.yaml b/tests/functional/commands/validate/lib/models/function_with_condition.yaml new file mode 100644 index 0000000000..059ea737b4 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_condition.yaml @@ -0,0 +1,13 @@ +Conditions: + TestCondition: + Fn::Equals: + - test + - test +Resources: + ConditionFunction: + Type: 'AWS::Serverless::Function' + Condition: "TestCondition" + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/function_with_custom_codedeploy_deployment_preference.yaml b/tests/functional/commands/validate/lib/models/function_with_custom_codedeploy_deployment_preference.yaml new file mode 100644 index 0000000000..1a6a8606a7 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_custom_codedeploy_deployment_preference.yaml @@ -0,0 +1,96 @@ +Mappings: + DeploymentPreferenceMap: + prod: + DeploymentPreference: + AllAtOnce + beta: + DeploymentPreference: + CustomDeployment + +Parameters: + Stage: + Type: String + Default: 'beta' + Deployment: + Type: String + Default: 'AllAtOnce' + Custom: + Type: String + Default: 'CustomDeployment' + +Conditions: + MyCondition: + Fn::Equals: + - true + - false + +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: TestDeploymentConfiguration + + CustomWithFindInMap: # Doesn't work + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: !FindInMap [DeploymentPreferenceMap, !Ref Stage, DeploymentPreference] + + CustomWithCondition: # Works + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: !If [MyCondition, TestDeploymentConfiguration, AllAtOnce] + + CustomWithCondition2: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: !If [MyCondition, !Sub "${Deployment}", !Ref Custom] + + NormalWithSub: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: !Sub ${Deployment} + + CustomWithSub: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: !Sub ${Custom} + + NormalWithRef: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: !Ref Deployment \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/function_with_custom_conditional_codedeploy_deployment_preference.yaml b/tests/functional/commands/validate/lib/models/function_with_custom_conditional_codedeploy_deployment_preference.yaml new file mode 100644 index 0000000000..e1a53328eb --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_custom_conditional_codedeploy_deployment_preference.yaml @@ -0,0 +1,19 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Parameters: + EnvType: + Default: dev + Type: String +Conditions: + IsDevEnv: !Equals [!Ref EnvType, dev] + IsDevEnv2: !Equals [!Ref EnvType, prod] +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs8.10 + CodeUri: s3://bucket/key + AutoPublishAlias: live + DeploymentPreference: + Type: !If [IsDevEnv, !If [IsDevEnv2, AllAtOnce, TestCustomDeploymentConfig], Canary10Percent15Minutes] diff --git a/tests/functional/commands/validate/lib/models/function_with_deployment_and_custom_role.yaml b/tests/functional/commands/validate/lib/models/function_with_deployment_and_custom_role.yaml new file mode 100644 index 0000000000..448b0fdc05 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_deployment_and_custom_role.yaml @@ -0,0 +1,40 @@ +Globals: + Function: + AutoPublishAlias: live + DeploymentPreference: + Type: AllAtOnce + +Resources: + + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + FunctionWithRole: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Role: !GetAtt DeploymentRole.Arn + + DeploymentRole: + Type: AWS::IAM::Role + Properties: + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + Service: + - codedeploy.amazonaws.com + + + diff --git a/tests/functional/commands/validate/lib/models/function_with_deployment_no_service_role.yaml b/tests/functional/commands/validate/lib/models/function_with_deployment_no_service_role.yaml new file mode 100644 index 0000000000..ae40a6a00b --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_deployment_no_service_role.yaml @@ -0,0 +1,40 @@ +Globals: + Function: + AutoPublishAlias: live + DeploymentPreference: + Type: AllAtOnce + Role: !Ref DeploymentRole + +Resources: + + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + OtherFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + DeploymentRole: + Type: AWS::IAM::Role + Properties: + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + Service: + - codedeploy.amazonaws.com + + + diff --git a/tests/functional/commands/validate/lib/models/function_with_deployment_preference.yaml b/tests/functional/commands/validate/lib/models/function_with_deployment_preference.yaml new file mode 100644 index 0000000000..19eaa94311 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_deployment_preference.yaml @@ -0,0 +1,10 @@ +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: Linear10PercentEvery3Minutes \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/function_with_deployment_preference_all_parameters.yaml b/tests/functional/commands/validate/lib/models/function_with_deployment_preference_all_parameters.yaml new file mode 100644 index 0000000000..00fdce01a9 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_deployment_preference_all_parameters.yaml @@ -0,0 +1,44 @@ +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Enabled: true + Type: Linear10PercentEvery1Minute + Hooks: + PreTraffic: !Ref MySanityTestFunction + PostTraffic: !Ref MyValidationTestFunction + Alarms: + - !Ref MyCloudWatchAlarm + + MySanityTestFunction: + Type: 'AWS::Serverless::Function' + Properties: + Handler: hello.handler + Runtime: python2.7 + CodeUri: s3://my-bucket/mySanityTestFunction.zip + DeploymentPreference: + Enabled: false + + MyValidationTestFunction: + Type: 'AWS::Serverless::Function' + Properties: + Handler: hello.handler + Runtime: python2.7 + CodeUri: s3://my-bucket/myValidationTestFunction.zip + DeploymentPreference: + Enabled: false + + MyCloudWatchAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + ComparisonOperator: GreaterThanThreshold + EvaluationPeriods: 1 + MetricName: MyMetric + Namespace: AWS/EC2 + Period: 300 + Threshold: 10 diff --git a/tests/functional/commands/validate/lib/models/function_with_deployment_preference_multiple_combinations.yaml b/tests/functional/commands/validate/lib/models/function_with_deployment_preference_multiple_combinations.yaml new file mode 100644 index 0000000000..588983aa04 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_deployment_preference_multiple_combinations.yaml @@ -0,0 +1,61 @@ +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + + MinimalFunctionWithMinimalDeploymentPreference: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: livewithdeployment + DeploymentPreference: + Type: Canary10Percent5Minutes + + MinimalFunctionWithDeploymentPreferenceWithHooksAndAlarms: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: livewithdeploymentwithhooksandalarms + DeploymentPreference: + Type: Linear10PercentEvery2Minutes + Hooks: + PreTraffic: !Ref MySanityTestFunction + PostTraffic: !Ref MyValidationTestFunction + Alarms: + - !Ref MyCloudWatchAlarm + + MySanityTestFunction: + Type: 'AWS::Serverless::Function' + Properties: + Handler: hello.handler + Runtime: python2.7 + CodeUri: s3://my-bucket/mySanityTestFunction.zip + DeploymentPreference: + Enabled: false + + MyValidationTestFunction: + Type: 'AWS::Serverless::Function' + Properties: + Handler: hello.handler + Runtime: python2.7 + CodeUri: s3://my-bucket/myValidationTestFunction.zip + DeploymentPreference: + Enabled: false + + MyCloudWatchAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + ComparisonOperator: GreaterThanThreshold + EvaluationPeriods: 1 + MetricName: MyMetric + Namespace: AWS/EC2 + Period: 300 + Threshold: 10 diff --git a/tests/functional/commands/validate/lib/models/function_with_disabled_deployment_preference.yaml b/tests/functional/commands/validate/lib/models/function_with_disabled_deployment_preference.yaml new file mode 100644 index 0000000000..092fd455ed --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_disabled_deployment_preference.yaml @@ -0,0 +1,11 @@ +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Enabled: false + Type: AllAtOnce \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/function_with_dlq.yaml b/tests/functional/commands/validate/lib/models/function_with_dlq.yaml new file mode 100644 index 0000000000..217777345b --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_dlq.yaml @@ -0,0 +1,21 @@ +Transform: "AWS::Serverless-2016-10-31" +Resources: + MySnsDlqLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: python2.7 + CodeUri: s3://sam-demo-bucket/hello.zip + DeadLetterQueue: + Type: SNS + TargetArn: arn + + MySqsDlqLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: python2.7 + CodeUri: s3://sam-demo-bucket/hello.zip + DeadLetterQueue: + Type: SQS + TargetArn: arn diff --git a/tests/functional/commands/validate/lib/models/function_with_global_layers.yaml b/tests/functional/commands/validate/lib/models/function_with_global_layers.yaml new file mode 100644 index 0000000000..2b4d6eb32f --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_global_layers.yaml @@ -0,0 +1,19 @@ +Globals: + Function: + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:layer1:1 + - arn:aws:lambda:us-east-1:123456789101:layer:layer2:1 + +# Note: there is a limit to the number of layers that a function can reference. +Resources: + ManyLayersFunc: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.6 + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:layer3:1 + - arn:aws:lambda:us-east-1:123456789101:layer:layer4:1 + - arn:aws:lambda:us-east-1:123456789101:layer:layer5:1 + diff --git a/tests/functional/commands/validate/lib/models/function_with_kmskeyarn.yaml b/tests/functional/commands/validate/lib/models/function_with_kmskeyarn.yaml new file mode 100644 index 0000000000..4679b19a06 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_kmskeyarn.yaml @@ -0,0 +1,34 @@ +Resources: + FunctionWithKeyArn: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + KmsKeyArn: thisIsaKey + + FunctionWithReferenceToKeyArn: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + KmsKeyArn: + Ref: myKey + + myKey: + Type: "AWS::KMS::Key" + Properties: + Description: "A sample key" + KeyPolicy: + Version: "2012-10-17" + Id: "key-default-1" + Statement: + - + Sid: "Allow administration of the key" + Effect: "Allow" + Principal: + AWS: "arn:aws:iam::123456789012:user/Alice" + Action: + - "kms:Create*" + Resource: "*" diff --git a/tests/functional/commands/validate/lib/models/function_with_layers.yaml b/tests/functional/commands/validate/lib/models/function_with_layers.yaml new file mode 100644 index 0000000000..1e34324d02 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_layers.yaml @@ -0,0 +1,42 @@ +Resources: + MinimalLayerFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1 + + FunctionNoLayerVersion: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:CorpXLayer:1 + + FunctionLayerWithSubIntrinsic: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:layer:CorpXLayer:1 + - Fn::Sub: arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:layer:CorpYLayer:1 + + FunctionReferencesLayer: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - !Ref MyLayer + + MyLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip diff --git a/tests/functional/commands/validate/lib/models/function_with_many_layers.yaml b/tests/functional/commands/validate/lib/models/function_with_many_layers.yaml new file mode 100644 index 0000000000..78ba2bc4ab --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_many_layers.yaml @@ -0,0 +1,18 @@ +Resources: + ManyLayersFunc: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Layers: + - arn:aws:lambda:us-east-1:123456789101:layer:z:1 + - !Sub arn:aws:lambda:${AWS::Region}:123456789101:layer:a:1 + - arn:aws:lambda:us-east-1:123456789101:layer:d12345678:1 + - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:123456789101:layer:c:1 + - !Ref MyLayer + + MyLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip diff --git a/tests/functional/commands/validate/lib/models/function_with_permissions_boundary.yaml b/tests/functional/commands/validate/lib/models/function_with_permissions_boundary.yaml new file mode 100644 index 0000000000..5cad58b7f9 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_permissions_boundary.yaml @@ -0,0 +1,9 @@ +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + PermissionsBoundary: arn:aws:1234:iam:boundary/CustomerCreatedPermissionsBoundary + diff --git a/tests/functional/commands/validate/lib/models/function_with_policy_templates.yaml b/tests/functional/commands/validate/lib/models/function_with_policy_templates.yaml new file mode 100644 index 0000000000..34fd86683f --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_policy_templates.yaml @@ -0,0 +1,55 @@ +Parameters: + FunctionNameParam: + Type: String + +Resources: + + OnePolicyTemplate: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Policies: + SQSPollerPolicy: + QueueName: + Fn::Sub: ["Some${value}", {"value": "KeyId"}] + + # Extra parameters will be skipped, and not appear in output + ExtraParam1: Value1 + ExtraParam2: Value2 + + MultiplePolicyTemplates: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Policies: + - SQSPollerPolicy: + QueueName: "Somekey" + - LambdaInvokePolicy: + FunctionName: "Some function" + + AllCombinations: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Policies: + # Inline statement + - Statement: + - Action: [ 'dynamodb:*' ] + Effect: Allow + Resource: '*' + # Regular managed policy + - AmazonDynamoDBFullAccess + + - LambdaInvokePolicy: + FunctionName: + # Refer to something + Ref: FunctionNameParam + + # Regular managed policy + - AWSLambdaRole diff --git a/tests/functional/commands/validate/lib/models/function_with_resource_refs.yaml b/tests/functional/commands/validate/lib/models/function_with_resource_refs.yaml new file mode 100644 index 0000000000..02732661c6 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_resource_refs.yaml @@ -0,0 +1,52 @@ +# Test to verify that resource references available on the Function are properly resolved +# Currently supported references are: +# - Alias +# +# Use them by appending the property to LogicalId of the function + +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + + FunctionWithoutAlias: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + MyBucket: + Type: AWS::S3::Bucket + Properties: + Name: + Fn::GetAtt: ["MinimalFunction.Alias", "Name"] + +Outputs: + AliasArn: + Value: !Ref MinimalFunction.Alias + + AliasName: + Value: !GetAtt MinimalFunction.Alias.Name + + # Alias doesn't exist for this function. This reference must not resolve + MustNotResolve: + Value: !GetAtt FunctionWithoutAlias.Alias.Name + + AliasInSub: + Value: + Fn::Sub: ["Hello ${MinimalFunction.Alias} ${MinimalFunction.Alias.Name} ${SomeValue}", {"SomeValue": "World"}] + + VersionArn: + Value: !Ref MinimalFunction.Version + VersionNumber: + Value: + Fn::GetAtt: ["MinimalFunction.Version", "Version"] + + UnResolvedVersion: + Value: + Ref: FunctionWithoutAlias.Version diff --git a/tests/functional/commands/validate/lib/models/function_with_sns_event_source_all_parameters.yaml b/tests/functional/commands/validate/lib/models/function_with_sns_event_source_all_parameters.yaml new file mode 100644 index 0000000000..a745b2f9c8 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_sns_event_source_all_parameters.yaml @@ -0,0 +1,28 @@ +Resources: + MyAwesomeFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + Events: + NotificationTopic: + Type: SNS + Properties: + Topic: topicArn + FilterPolicy: + store: + - example_corp + event: + - anything-but: order_cancelled + customer_interests: + - rugby + - football + - baseball + price_usd: + - numeric: + - ">=" + - 100 + + diff --git a/tests/functional/commands/validate/lib/models/global_handle_path_level_parameter.yaml b/tests/functional/commands/validate/lib/models/global_handle_path_level_parameter.yaml new file mode 100644 index 0000000000..2bd9995fbf --- /dev/null +++ b/tests/functional/commands/validate/lib/models/global_handle_path_level_parameter.yaml @@ -0,0 +1,64 @@ +Globals: + Api: + Name: "some api" + CacheClusterEnabled: True + CacheClusterSize: "1.6" + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + Variables: + SomeVar: Value + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: SomeStage + DefinitionBody: + swagger: 2.0 + info: + version: '1.0' + title: !Ref AWS::StackName + paths: + "/": + parameters: + - name: domain + in: path + description: Application domain + type: string + required: true + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations + responses: {} + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/globals_for_api.yaml b/tests/functional/commands/validate/lib/models/globals_for_api.yaml new file mode 100644 index 0000000000..5d3a956322 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/globals_for_api.yaml @@ -0,0 +1,58 @@ +Globals: + Api: + Name: "some api" + CacheClusterEnabled: True + CacheClusterSize: "1.6" + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + Variables: + SomeVar: Value + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: SomeStage + DefinitionBody: + swagger: 2.0 + info: + version: '1.0' + title: !Ref AWS::StackName + paths: + "/": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations + responses: {} + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/globals_for_function.yaml b/tests/functional/commands/validate/lib/models/globals_for_function.yaml new file mode 100644 index 0000000000..f3acd0e898 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/globals_for_function.yaml @@ -0,0 +1,52 @@ +Globals: + Function: + CodeUri: s3://global-bucket/global.zip + Handler: hello.handler + Runtime: python2.7 + MemorySize: 1024 + Timeout: 30 + VpcConfig: + SecurityGroupIds: + - sg-edcd9784 + SubnetIds: + - sub-id-2 + Environment: + Variables: + Var1: value1 + Var2: value2 + Tags: + tag1: value1 + Tracing: Active + AutoPublishAlias: live + PermissionsBoundary: arn:aws:1234:iam:boundary/CustomerCreatedPermissionsBoundary + Layers: + - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:layer:MyLayer:1 + ReservedConcurrentExecutions: 50 + +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + + FunctionWithOverrides: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs4.3 + MemorySize: 512 + Timeout: 100 + VpcConfig: + SecurityGroupIds: + - sg-123 + Environment: + Variables: + Var3: value3 + Tags: + newtag1: newvalue1 + Tracing: PassThrough + AutoPublishAlias: prod + PermissionsBoundary: arn:aws:1234:iam:boundary/OverridePermissionsBoundary + Layers: + - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:layer:MyLayer2:2 + ReservedConcurrentExecutions: 100 + diff --git a/tests/functional/commands/validate/lib/models/globals_for_function_path.yaml b/tests/functional/commands/validate/lib/models/globals_for_function_path.yaml new file mode 100644 index 0000000000..9eaa179fae --- /dev/null +++ b/tests/functional/commands/validate/lib/models/globals_for_function_path.yaml @@ -0,0 +1,51 @@ +Globals: + Function: + CodeUri: ./ + Handler: hello.handler + Runtime: python2.7 + MemorySize: 1024 + Timeout: 30 + VpcConfig: + SecurityGroupIds: + - sg-edcd9784 + SubnetIds: + - sub-id-2 + Environment: + Variables: + Var1: value1 + Var2: value2 + Tags: + tag1: value1 + Tracing: Active + AutoPublishAlias: live + PermissionsBoundary: arn:aws:1234:iam:boundary/CustomerCreatedPermissionsBoundary + Layers: + - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:layer:MyLayer:1 + ReservedConcurrentExecutions: 50 + +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + + FunctionWithOverrides: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs4.3 + MemorySize: 512 + Timeout: 100 + VpcConfig: + SecurityGroupIds: + - sg-123 + Environment: + Variables: + Var3: value3 + Tags: + newtag1: newvalue1 + Tracing: PassThrough + AutoPublishAlias: prod + PermissionsBoundary: arn:aws:1234:iam:boundary/OverridePermissionsBoundary + Layers: + - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:layer:MyLayer2:2 + ReservedConcurrentExecutions: 100 diff --git a/tests/functional/commands/validate/lib/models/globals_for_simpletable.yaml b/tests/functional/commands/validate/lib/models/globals_for_simpletable.yaml new file mode 100644 index 0000000000..6a03a75ed7 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/globals_for_simpletable.yaml @@ -0,0 +1,8 @@ +Globals: + SimpleTable: + SSESpecification: + SSEEnabled: true + +Resources: + MinimalTable: + Type: AWS::Serverless::SimpleTable \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/implicit_and_explicit_api_with_conditions.yaml b/tests/functional/commands/validate/lib/models/implicit_and_explicit_api_with_conditions.yaml new file mode 100644 index 0000000000..5303f53be5 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/implicit_and_explicit_api_with_conditions.yaml @@ -0,0 +1,93 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: A template to test for API condition handling with a mix of explicit and implicit api events. +Conditions: + implicithello1condition: + Fn::Equals: + - true + - false + implicithello2condition: + Fn::Equals: + - true + - false + explicithello1condition: + Fn::Equals: + - true + - false + explicithello2condition: + Fn::Equals: + - true + - false + +Resources: + implicithello1: + Type: 'AWS::Serverless::Function' + Condition: implicithello1condition + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /implicit/hello1 + Method: get + implicithello2: + Type: 'AWS::Serverless::Function' + Condition: implicithello2condition + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /implicit/hello2 + Method: get + + explicitapi: + Type: 'AWS::Serverless::Api' + Properties: + StageName: Prod + explicithello1: + Type: 'AWS::Serverless::Function' + Condition: explicithello1condition + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + RestApiId: !Ref explicitapi + Path: /explicit/hello1 + Method: get + explicithello2: + Type: 'AWS::Serverless::Function' + Condition: explicithello2condition + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + RestApiId: !Ref explicitapi + Path: /explicit/hello2 + Method: get diff --git a/tests/functional/commands/validate/lib/models/implicit_api.yaml b/tests/functional/commands/validate/lib/models/implicit_api.yaml new file mode 100644 index 0000000000..c5b6422249 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/implicit_api.yaml @@ -0,0 +1,38 @@ +Resources: + RestApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.restapi + Runtime: nodejs4.3 + Policies: AmazonDynamoDBFullAccess + Events: + AddItem: + Type: Api + Properties: + Path: /add + Method: post + CompleteItem: + Type: Api + Properties: + Path: /complete + Method: POST + GetList: + Type: Api + Properties: + Path: /getlist + Method: get + + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Policies: AmazonDynamoDBReadOnlyAccess + Events: + GetHtml: + Type: Api + Properties: + Path: /{proxy+} + Method: any diff --git a/tests/functional/commands/validate/lib/models/implicit_api_with_auth_and_conditions_max.yaml b/tests/functional/commands/validate/lib/models/implicit_api_with_auth_and_conditions_max.yaml new file mode 100644 index 0000000000..61d2751a21 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/implicit_api_with_auth_and_conditions_max.yaml @@ -0,0 +1,164 @@ +Globals: + Api: + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: arn:aws:1 + Identity: + Header: MyAuthorizationHeader + ValidationExpression: myauthvalidationexpression + + MyCognitoAuthMultipleUserPools: + UserPoolArn: + - arn:aws:2 + - arn:aws:3 + Identity: + Header: MyAuthorizationHeader2 + ValidationExpression: myauthvalidationexpression2 + + MyLambdaTokenAuth: + FunctionPayloadType: TOKEN + FunctionArn: arn:aws + FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access + Identity: + Header: MyCustomAuthHeader + ValidationExpression: mycustomauthexpression + ReauthorizeEvery: 20 + + MyLambdaTokenAuthNoneFunctionInvokeRole: + FunctionArn: arn:aws + FunctionInvokeRole: NONE + Identity: + ReauthorizeEvery: 0 + + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: arn:aws + FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access + Identity: + Headers: + - Authorization1 + QueryStrings: + - Authorization2 + StageVariables: + - Authorization3 + Context: + - Authorization4 + ReauthorizeEvery: 0 + +Conditions: + FunctionCondition: + Fn::Equals: + - true + - false + FunctionCondition2: + Fn::Equals: + - true + - false + FunctionCondition3: + Fn::Equals: + - true + - false + FunctionCondition4: + Fn::Equals: + - true + - false + FunctionCondition5: + Fn::Equals: + - true + - false + FunctionCondition6: + Fn::Equals: + - true + - false + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Condition: FunctionCondition + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithNoAuthorizer: + Type: Api + Properties: + Path: / + Method: get + Auth: + Authorizer: NONE + MyFunction2: + Type: AWS::Serverless::Function + Condition: FunctionCondition2 + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithCognitoMultipleUserPoolsAuthorizer: + Type: Api + Properties: + Path: /users + Method: post + Auth: + Authorizer: MyCognitoAuthMultipleUserPools + MyFunction3: + Type: AWS::Serverless::Function + Condition: FunctionCondition3 + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithLambdaTokenAuthorizer: + Type: Api + Properties: + Path: /users + Method: get + Auth: + Authorizer: MyLambdaTokenAuth + MyFunction4: + Type: AWS::Serverless::Function + Condition: FunctionCondition4 + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithLambdaTokenAuthorizer: + Type: Api + Properties: + Path: /users + Method: patch + Auth: + Authorizer: MyLambdaTokenAuthNoneFunctionInvokeRole + MyFunction5: + Type: AWS::Serverless::Function + Condition: FunctionCondition5 + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithLambdaRequestAuthorizer: + Type: Api + Properties: + Path: /users + Method: delete + Auth: + Authorizer: MyLambdaRequestAuth + MyFunction6: + Type: AWS::Serverless::Function + Condition: FunctionCondition6 + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + WithDefaultAuthorizer: + Type: Api + Properties: + Path: /users + Method: put \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/implicit_api_with_many_conditions.yaml b/tests/functional/commands/validate/lib/models/implicit_api_with_many_conditions.yaml new file mode 100644 index 0000000000..9ee7cc3deb --- /dev/null +++ b/tests/functional/commands/validate/lib/models/implicit_api_with_many_conditions.yaml @@ -0,0 +1,226 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: A template to test for implicit API condition handling. +Conditions: + MyCondition: + Fn::Equals: + - true + - false + Cond: + Fn::Equals: + - true + - false + Cond1: + Fn::Equals: + - true + - false + Cond2: + Fn::Equals: + - true + - false + Cond3: + Fn::Equals: + - true + - false + Cond4: + Fn::Equals: + - true + - false + Cond5: + Fn::Equals: + - true + - false + Cond6: + Fn::Equals: + - true + - false + Cond7: + Fn::Equals: + - true + - false + Cond8: + Fn::Equals: + - true + - false + Cond9: + Fn::Equals: + - true + - false + +Resources: + hello: + Type: 'AWS::Serverless::Function' + Condition: MyCondition + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub + Method: get + helloworld: + Type: 'AWS::Serverless::Function' + Condition: Cond + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub + Method: post + helloworld1: + Type: 'AWS::Serverless::Function' + Condition: Cond1 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub1 + Method: post + helloworld2: + Type: 'AWS::Serverless::Function' + Condition: Cond2 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub2 + Method: post + helloworld3: + Type: 'AWS::Serverless::Function' + Condition: Cond3 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub3 + Method: post + helloworld4: + Type: 'AWS::Serverless::Function' + Condition: Cond4 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub4 + Method: post + helloworld5: + Type: 'AWS::Serverless::Function' + Condition: Cond5 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub5 + Method: post + helloworld6: + Type: 'AWS::Serverless::Function' + Condition: Cond6 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub6 + Method: post + helloworld7: + Type: 'AWS::Serverless::Function' + Condition: Cond7 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub7 + Method: post + helloworld8: + Type: 'AWS::Serverless::Function' + Condition: Cond8 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub8 + Method: post + helloworld9: + Type: 'AWS::Serverless::Function' + Condition: Cond9 + Properties: + Handler: index.handler + Runtime: nodejs8.10 + MemorySize: 128 + Timeout: 3 + InlineCode: | + exports.handler = async () => ‘Hello World!' + Events: + ApiEvent: + Type: Api + Properties: + Path: /sub9 + Method: post diff --git a/tests/functional/commands/validate/lib/models/implicit_api_with_serverless_rest_api_resource.yaml b/tests/functional/commands/validate/lib/models/implicit_api_with_serverless_rest_api_resource.yaml new file mode 100644 index 0000000000..be49ab4645 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/implicit_api_with_serverless_rest_api_resource.yaml @@ -0,0 +1,45 @@ +Resources: + RestApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.restapi + Runtime: nodejs4.3 + Policies: AmazonDynamoDBFullAccess + Events: + AddItem: + Type: Api + Properties: + Path: /add + Method: post + CompleteItem: + Type: Api + Properties: + Path: /complete + Method: POST + GetList: + Type: Api + Properties: + Path: /getlist + Method: get + + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Policies: AmazonDynamoDBReadOnlyAccess + Events: + GetHtml: + Type: Api + Properties: + Path: /{proxy+} + Method: any + + ServerlessRestApi: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 diff --git a/tests/functional/commands/validate/lib/models/intrinsic_functions.yaml b/tests/functional/commands/validate/lib/models/intrinsic_functions.yaml new file mode 100644 index 0000000000..a17a2378c1 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/intrinsic_functions.yaml @@ -0,0 +1,173 @@ +# SAM template using intrinsic function on every property that supports it. +# Translator should handle it properly +Parameters: + CodeBucket: + Type: String + Default: sam-demo-bucket + FunctionName: + Type: String + Default: MySuperFunctionName + TracingConfigParam: + Type: String + Default: PassThrough + MyExplicitApiName: + Type: String + Default: SomeName + CodeKey: + Type: String + Default: "key" + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + +Resources: + MyFunction: + Type: 'AWS::Serverless::Function' + Properties: + FunctionName: + Ref: FunctionName + CodeUri: + Bucket: + Ref: CodeBucket + Key: + "Fn::Sub": "code.zip.${CodeKey}" + Version: + "Fn::Join": ["", ["some", "version"]] + + Handler: + "Fn::Sub": ["${filename}.handler", {filename: "index"}] + + Runtime: + "Fn::Join": ["", ["nodejs", "4.3"]] + + Role: + "Fn::GetAtt": ["MyNewRole", "Arn"] + + Tracing: + Ref: TracingConfigParam + + Events: + MyApi: + Type: Api + Properties: + Path: / + Method: GET + RestApiId: + "Ref": "MyExplicitApi" + + MyExplicitApi: + Type: AWS::Serverless::Api + Properties: + Name: + Ref: MyExplicitApiName + DefinitionUri: s3://sam-demo-bucket/swagger.yaml + StageName: dev + Variables: + FunctionName: + Fn::Sub: "${MyFunction}" + Var2: + Fn::Join: ["join ", ["some value ", "with some other value"]] + + FunctionWithExplicitS3Uri: + Type: AWS::Serverless::Function + Properties: + Handler: stream.ddb_handler + Runtime: python2.7 + CodeUri: + Bucket: mybucket + Key: mykey + Version: MyVersion + + ApiWithExplicitS3Uri: + Type: AWS::Serverless::Api + Condition: TrueCondition + Properties: + StageName: dev + DefinitionUri: + Bucket: mybucket + Key: mykey + Version: myversion + + DynamoDBFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/streams.zip + Handler: stream.ddb_handler + Runtime: python2.7 + Events: + MyDDBStream: + Type: DynamoDB + Properties: + Stream: + Fn::GetAtt: [MyTable, "StreamArn"] + BatchSize: 200 + StartingPosition: LATEST + + MyTable: + Type: AWS::DynamoDB::Table + Properties: + + AttributeDefinitions: + - { AttributeName : id, AttributeType : S } + + KeySchema: + - { "AttributeName" : "id", "KeyType" : "HASH"} + + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + + StreamSpecification: + StreamViewType: "NEW_IMAGE" + + MySqsDlqLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: python2.7 + CodeUri: s3://sam-demo-bucket/hello.zip + DeadLetterQueue: + Type: SQS + TargetArn: + Fn::GetAtt: + - SqsDlqQueue + - Arn + + SqsDlqQueue: + Type: AWS::SQS::Queue + + MySnsDlqLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: python2.7 + CodeUri: s3://sam-demo-bucket/hello.zip + DeadLetterQueue: + Type: SNS + TargetArn: + Ref: SnsDlqQueue + + SnsDlqQueue: + Type: AWS::SNS::Topic + + MyNewRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: ['sts:AssumeRole'] + Effect: Allow + Principal: + Service: [lambda.amazonaws.com] + Version: '2012-10-17' + Policies: + - PolicyDocument: + Statement: + - Action: ['cloudwatch:*', 'logs:*'] + Effect: Allow + Resource: '*' + Version: '2012-10-17' + PolicyName: lambdaRole diff --git a/tests/functional/commands/validate/lib/models/iot_rule.yaml b/tests/functional/commands/validate/lib/models/iot_rule.yaml new file mode 100644 index 0000000000..3d3b50cbcd --- /dev/null +++ b/tests/functional/commands/validate/lib/models/iot_rule.yaml @@ -0,0 +1,26 @@ +# File: sam.yml +# Version: 0.9 + +AWSTemplateFormatVersion: '2010-09-09' +Parameters: {} +Resources: + IoTRuleFunc: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Description: Created by SAM + Events: + MyIoTRule: + Type: IoTRule + Properties: + Sql: SELECT * FROM 'topic/test' + AwsIotSqlVersion: beta + MyOtherIoTRule: + Type: IoTRule + Properties: + Sql: SELECT * FROM 'topic/test' + Handler: index.handler + MemorySize: 1024 + Runtime: nodejs4.3 + Timeout: 3 + diff --git a/tests/functional/commands/validate/lib/models/layers_all_properties.yaml b/tests/functional/commands/validate/lib/models/layers_all_properties.yaml new file mode 100644 index 0000000000..03fef5dbc2 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/layers_all_properties.yaml @@ -0,0 +1,38 @@ +Parameters: + LayerDeleteParam: + Type: String + Default: Delete + +Resources: + MyLayer: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://bucket/key + RetentionPolicy: !Ref LayerDeleteParam + + MyLayerWithAName: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://bucket/key + LayerName: DifferentLayerName + + MyFunction: + Type: "AWS::Serverless::Function" + Properties: + CodeUri: s3://bucket/key + Handler: app.handler + Runtime: python3.6 + Layers: + - !Ref MyLayer + +Outputs: + LayerName: + Value: !Ref MyLayer + FunctionName: + Value: !Ref MyFunction + FunctionAtt: + Value: !GetAtt MyFunction.Arn + LayerSub: + Value: !Sub ${MyLayer} + FunctionSub: + Value: !Sub ${MyFunction} diff --git a/tests/functional/commands/validate/lib/models/layers_with_intrinsics.yaml b/tests/functional/commands/validate/lib/models/layers_with_intrinsics.yaml new file mode 100644 index 0000000000..70a9c8764f --- /dev/null +++ b/tests/functional/commands/validate/lib/models/layers_with_intrinsics.yaml @@ -0,0 +1,48 @@ +Parameters: + LayerNameParam: + Type: String + Default: SomeLayerName + LayerLicenseInfo: + Type: String + Default: MIT-0 License + LayerRuntimeList: + Type: CommaDelimitedList + +Resources: + LayerWithLicenseIntrinsic: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + LicenseInfo: !Ref LayerLicenseInfo + + LayerWithRuntimesIntrinsic: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + CompatibleRuntimes: + Ref: LayerRuntimeList + + LayerWithNameIntrinsic: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + LayerName: !Ref LayerNameParam + + LayerWithSubNameIntrinsic: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + LayerName: !Sub layer-${LayerNameParam} + + LayerWithRefNameIntrinsicRegion: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + LayerName: !Ref 'AWS::Region' + + LayerWithSubNameIntrinsicRegion: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + LayerName: !Sub 'layer-${AWS::Region}' + diff --git a/tests/functional/commands/validate/lib/models/no_implicit_api_with_serverless_rest_api_resource.yaml b/tests/functional/commands/validate/lib/models/no_implicit_api_with_serverless_rest_api_resource.yaml new file mode 100644 index 0000000000..0998b13b38 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/no_implicit_api_with_serverless_rest_api_resource.yaml @@ -0,0 +1,74 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + ServerlessRestApi: + Type: AWS::ApiGateway::RestApi + Properties: + Body: + info: + version: '1.0' + title: + Ref: AWS::StackName + paths: + "/add": + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations + responses: {} + "/{proxy+}": + x-amazon-apigateway-any-method: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations + responses: {} + "/getlist": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations + responses: {} + "/complete": + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations + responses: {} + swagger: '2.0' + + Images: + Type: AWS::S3::Bucket + + RestApiFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.6 + + GetHtmlFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python3.6 \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/s3.yaml b/tests/functional/commands/validate/lib/models/s3.yaml new file mode 100644 index 0000000000..cfead59f4c --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3.yaml @@ -0,0 +1,17 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket diff --git a/tests/functional/commands/validate/lib/models/s3_create_remove.yaml b/tests/functional/commands/validate/lib/models/s3_create_remove.yaml new file mode 100644 index 0000000000..af1abaad68 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3_create_remove.yaml @@ -0,0 +1,25 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: + - s3:ObjectCreated:* + - s3:ObjectRemoved:* + + Images: + Type: AWS::S3::Bucket + Properties: + BucketName: "BucketNameParameter" + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 diff --git a/tests/functional/commands/validate/lib/models/s3_existing_lambda_notification_configuration.yaml b/tests/functional/commands/validate/lib/models/s3_existing_lambda_notification_configuration.yaml new file mode 100644 index 0000000000..fd9ef06f75 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3_existing_lambda_notification_configuration.yaml @@ -0,0 +1,23 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket + Properties: + NotificationConfiguration: + LambdaConfigurations: + - Function: + Fn::GetAtt: [ThumbnailFunction, Arn] + Event: s3:ObjectCreated:* diff --git a/tests/functional/commands/validate/lib/models/s3_existing_other_notification_configuration.yaml b/tests/functional/commands/validate/lib/models/s3_existing_other_notification_configuration.yaml new file mode 100644 index 0000000000..b181bb7a6d --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3_existing_other_notification_configuration.yaml @@ -0,0 +1,22 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket + Properties: + NotificationConfiguration: + TopicConfigurations: + - Topic: my-super-awesome-topic + Event: s3:ObjectRemoved:* diff --git a/tests/functional/commands/validate/lib/models/s3_filter.yaml b/tests/functional/commands/validate/lib/models/s3_filter.yaml new file mode 100644 index 0000000000..a9aa41b4a0 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3_filter.yaml @@ -0,0 +1,22 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + Filter: + S3Key: + Rules: + - Name: name + Value: value + + Images: + Type: AWS::S3::Bucket diff --git a/tests/functional/commands/validate/lib/models/s3_multiple_events_same_bucket.yaml b/tests/functional/commands/validate/lib/models/s3_multiple_events_same_bucket.yaml new file mode 100644 index 0000000000..330db80538 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3_multiple_events_same_bucket.yaml @@ -0,0 +1,28 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucketCreates: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + ImageBucketDeletes: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectRemoved:* + Filter: + S3Key: + Rules: + - Name: suffix + Value: .jpg + + Images: + Type: AWS::S3::Bucket diff --git a/tests/functional/commands/validate/lib/models/s3_multiple_functions.yaml b/tests/functional/commands/validate/lib/models/s3_multiple_functions.yaml new file mode 100644 index 0000000000..f452f8fb9f --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3_multiple_functions.yaml @@ -0,0 +1,37 @@ +Resources: + FunctionOne: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + FunctionTwo: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket + Properties: + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true diff --git a/tests/functional/commands/validate/lib/models/s3_with_condition.yaml b/tests/functional/commands/validate/lib/models/s3_with_condition.yaml new file mode 100644 index 0000000000..e83ec38f9d --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3_with_condition.yaml @@ -0,0 +1,23 @@ +Conditions: + MyCondition: + Fn::Equals: + - yes + - no +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Condition: MyCondition + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket diff --git a/tests/functional/commands/validate/lib/models/s3_with_dependsOn.yaml b/tests/functional/commands/validate/lib/models/s3_with_dependsOn.yaml new file mode 100644 index 0000000000..af5b92e513 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/s3_with_dependsOn.yaml @@ -0,0 +1,21 @@ +Resources: + Topic: + Type: AWS::SNS::Topic + + ThumbnailFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs4.3 + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket + DependsOn: Topic diff --git a/tests/functional/commands/validate/lib/models/simple_table_ref_parameter_intrinsic.yaml b/tests/functional/commands/validate/lib/models/simple_table_ref_parameter_intrinsic.yaml new file mode 100644 index 0000000000..55fcfdec87 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/simple_table_ref_parameter_intrinsic.yaml @@ -0,0 +1,22 @@ +Parameters: + ReadCapacity: + Type: Number + Default: 15 + WriteCapacity: + Type: Number + Default: 15 + EnableSSE: + Type: String # Boolean parameter types not allowed + Default: True +Resources: + MinimalTableRefParamLongForm: + Type: 'AWS::Serverless::SimpleTable' + Properties: + ProvisionedThroughput: + ReadCapacityUnits: + Ref: ReadCapacity + WriteCapacityUnits: + Ref: WriteCapacity + SSESpecification: + SSEEnabled: + Ref: EnableSSE \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/simple_table_with_extra_tags.yaml b/tests/functional/commands/validate/lib/models/simple_table_with_extra_tags.yaml new file mode 100644 index 0000000000..d8daa60c04 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/simple_table_with_extra_tags.yaml @@ -0,0 +1,15 @@ +Parameters: + TagValueParam: + Type: String + Default: value + +Resources: + MinimalTableWithTags: + Type: 'AWS::Serverless::SimpleTable' + Properties: + Tags: + TagKey1: TagValue1 + TagKey2: "" + TagKey3: + Ref: TagValueParam + TagKey4: "123" diff --git a/tests/functional/commands/validate/lib/models/simple_table_with_table_name.yaml b/tests/functional/commands/validate/lib/models/simple_table_with_table_name.yaml new file mode 100644 index 0000000000..8055c0d709 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/simple_table_with_table_name.yaml @@ -0,0 +1,22 @@ +Parameters: + MySimpleTableParameter: + Type: String + Default: TableName + +Resources: + MinimalTableWithTableName: + Type: 'AWS::Serverless::SimpleTable' + Properties: + TableName: MySimpleTable + + MinimalTableWithRefTableName: + Type: 'AWS::Serverless::SimpleTable' + Properties: + TableName: + Ref: MySimpleTableParameter + + MinimalTableWithSubTableName: + Type: 'AWS::Serverless::SimpleTable' + Properties: + TableName: + Fn::Sub: "${AWS::StackName}MySimpleTable" \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/simpletable.yaml b/tests/functional/commands/validate/lib/models/simpletable.yaml new file mode 100644 index 0000000000..cfa0b39139 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/simpletable.yaml @@ -0,0 +1,12 @@ +Resources: + MinimalTable: + Type: 'AWS::Serverless::SimpleTable' + CompleteTable: + Type: 'AWS::Serverless::SimpleTable' + Properties: + PrimaryKey: + Name: member-number + Type: Number + ProvisionedThroughput: + ReadCapacityUnits: 20 + WriteCapacityUnits: 10 diff --git a/tests/functional/commands/validate/lib/models/simpletable_with_sse.yaml b/tests/functional/commands/validate/lib/models/simpletable_with_sse.yaml new file mode 100644 index 0000000000..b9c6560e85 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/simpletable_with_sse.yaml @@ -0,0 +1,6 @@ +Resources: + TableWithSSE: + Type: 'AWS::Serverless::SimpleTable' + Properties: + SSESpecification: + SSEEnabled: true \ No newline at end of file diff --git a/tests/functional/commands/validate/lib/models/sns.yaml b/tests/functional/commands/validate/lib/models/sns.yaml new file mode 100644 index 0000000000..95e05f6f3a --- /dev/null +++ b/tests/functional/commands/validate/lib/models/sns.yaml @@ -0,0 +1,16 @@ +Resources: + SaveNotificationFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/notifications.zip + Handler: index.save_notification + Runtime: nodejs4.3 + Events: + NotificationTopic: + Type: SNS + Properties: + Topic: + Ref: Notifications + + Notifications: + Type: AWS::SNS::Topic diff --git a/tests/functional/commands/validate/lib/models/sns_existing_other_subscription.yaml b/tests/functional/commands/validate/lib/models/sns_existing_other_subscription.yaml new file mode 100644 index 0000000000..a60462de92 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/sns_existing_other_subscription.yaml @@ -0,0 +1,20 @@ +Resources: + SaveNotificationFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/notifications.zip + Handler: index.save_notification + Runtime: nodejs4.3 + Events: + NotificationTopic: + Type: SNS + Properties: + Topic: + Ref: Notifications + + Notifications: + Type: AWS::SNS::Topic + Properties: + Subscription: + - Endpoint: my-queue-arn + Protocol: sqs diff --git a/tests/functional/commands/validate/lib/models/sns_topic_outside_template.yaml b/tests/functional/commands/validate/lib/models/sns_topic_outside_template.yaml new file mode 100644 index 0000000000..0de6c218ac --- /dev/null +++ b/tests/functional/commands/validate/lib/models/sns_topic_outside_template.yaml @@ -0,0 +1,16 @@ +Parameters: + SNSTopicArn: + Type: String +Resources: + SaveNotificationFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/notifications.zip + Handler: index.save_notification + Runtime: nodejs4.3 + Events: + NotificationTopic: + Type: SNS + Properties: + Topic: + Ref: SNSTopicArn diff --git a/tests/functional/commands/validate/lib/models/sqs.yaml b/tests/functional/commands/validate/lib/models/sqs.yaml new file mode 100644 index 0000000000..9d45de0a86 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/sqs.yaml @@ -0,0 +1,14 @@ +Resources: + SQSFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/queues.zip + Handler: queue.sqs_handler + Runtime: python2.7 + Events: + MySqsQueue: + Type: SQS + Properties: + Queue: arn:aws:sqs:us-west-2:012345678901:my-queue + BatchSize: 10 + Enabled: false diff --git a/tests/functional/commands/validate/lib/models/streams.yaml b/tests/functional/commands/validate/lib/models/streams.yaml new file mode 100644 index 0000000000..ff2b54da2c --- /dev/null +++ b/tests/functional/commands/validate/lib/models/streams.yaml @@ -0,0 +1,28 @@ +Resources: + KinesisFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/streams.zip + Handler: stream.kinesis_handler + Runtime: python2.7 + Events: + MyKinesisStream: + Type: Kinesis + Properties: + Stream: arn:aws:kinesis:us-west-2:012345678901:stream/my-stream + BatchSize: 100 + StartingPosition: TRIM_HORIZON + Enabled: false + DynamoDBFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/streams.zip + Handler: stream.ddb_handler + Runtime: python2.7 + Events: + MyDDBStream: + Type: DynamoDB + Properties: + Stream: arn:aws:dynamodb:us-west-2:012345678901:table/TestTable/stream/2015-05-11T21:21:33.291 + BatchSize: 200 + StartingPosition: LATEST diff --git a/tests/functional/commands/validate/lib/models/unsupported_resources.yaml b/tests/functional/commands/validate/lib/models/unsupported_resources.yaml new file mode 100644 index 0000000000..1a74058642 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/unsupported_resources.yaml @@ -0,0 +1,14 @@ +Resources: + # These resources are NOT errors from the eye of translator. + # Translator will simply ignore resources with unknown type in order to accept non-SAM resources + # in SAM template + ResourceMissingTypeShouldBeAccepted: + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json + + InvalidResourceTypeShouldBeAccepted: + Type: AWS::Serverless::ABCD + Properties: + StageName: Prod + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json diff --git a/tests/functional/commands/validate/lib/test_sam_template_validator.py b/tests/functional/commands/validate/lib/test_sam_template_validator.py index 0c21cb4821..fb8b42f284 100644 --- a/tests/functional/commands/validate/lib/test_sam_template_validator.py +++ b/tests/functional/commands/validate/lib/test_sam_template_validator.py @@ -1,5 +1,8 @@ from unittest import TestCase from mock import Mock +from parameterized import parameterized + +import samcli.yamlhelper as yamlhelper from samcli.commands.validate.lib.sam_template_validator import SamTemplateValidator from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException @@ -7,6 +10,122 @@ class TestValidate(TestCase): + VALID_TEST_TEMPLATES = [ + ("tests/functional/commands/validate/lib/models/alexa_skill.yaml"), + ("tests/functional/commands/validate/lib/models/alexa_skill_with_skill_id.yaml"), + ("tests/functional/commands/validate/lib/models/all_policy_templates.yaml"), + ("tests/functional/commands/validate/lib/models/api_cache.yaml"), + ("tests/functional/commands/validate/lib/models/api_endpoint_configuration.yaml"), + ("tests/functional/commands/validate/lib/models/api_request_model.yaml"), + ("tests/functional/commands/validate/lib/models/api_request_model_openapi_3.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_access_log_setting.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_auth_all_maximum.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_auth_all_maximum_openapi_3.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_auth_all_minimum.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_auth_all_minimum_openapi.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_auth_and_conditions_all_max.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_auth_no_default.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_aws_iam_auth_overrides.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_binary_media_types.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_binary_media_types_definition_body.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_canary_setting.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_and_auth_no_preflight_auth.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_and_auth_preflight_auth.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_and_conditions_no_definitionbody.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_and_only_credentials_false.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_and_only_headers.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_and_only_maxage.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_and_only_methods.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_and_only_origins.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_no_definitionbody.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_cors_openapi_3.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_default_aws_iam_auth.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_gateway_responses.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_gateway_responses_all.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_gateway_responses_all_openapi_3.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_gateway_responses_implicit.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_gateway_responses_minimal.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_gateway_responses_string_status_code.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_method_aws_iam_auth.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_method_settings.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_minimum_compression_size.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_open_api_version.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_open_api_version_2.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_openapi_definition_body_no_flag.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_resource_refs.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_swagger_and_openapi_with_auth.yaml"), + ("tests/functional/commands/validate/lib/models/api_with_xray_tracing.yaml"), + ("tests/functional/commands/validate/lib/models/basic_function_with_tags.yaml"), + ("tests/functional/commands/validate/lib/models/basic_layer.yaml"), + ("tests/functional/commands/validate/lib/models/cloudwatch_logs_with_ref.yaml"), + ("tests/functional/commands/validate/lib/models/cloudwatchevent.yaml"), + ("tests/functional/commands/validate/lib/models/cloudwatchlog.yaml"), + ("tests/functional/commands/validate/lib/models/depends_on.yaml"), + ("tests/functional/commands/validate/lib/models/explicit_api.yaml"), + ("tests/functional/commands/validate/lib/models/explicit_api_openapi_3.yaml"), + ("tests/functional/commands/validate/lib/models/explicit_api_with_invalid_events_config.yaml"), + ("tests/functional/commands/validate/lib/models/function_concurrency.yaml"), + ("tests/functional/commands/validate/lib/models/function_event_conditions.yaml"), + ("tests/functional/commands/validate/lib/models/function_managed_inline_policy.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_alias.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_alias_and_event_sources.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_alias_intrinsics.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_condition.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_custom_codedeploy_deployment_preference.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_custom_conditional_codedeploy_deployment_preference.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_deployment_and_custom_role.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_deployment_no_service_role.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_deployment_preference.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_deployment_preference_all_parameters.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_deployment_preference_multiple_combinations.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_disabled_deployment_preference.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_dlq.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_global_layers.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_kmskeyarn.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_layers.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_many_layers.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_permissions_boundary.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_policy_templates.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_resource_refs.yaml"), + ("tests/functional/commands/validate/lib/models/function_with_sns_event_source_all_parameters.yaml"), + ("tests/functional/commands/validate/lib/models/global_handle_path_level_parameter.yaml"), + ("tests/functional/commands/validate/lib/models/globals_for_api.yaml"), + ("tests/functional/commands/validate/lib/models/globals_for_function.yaml"), + ("tests/functional/commands/validate/lib/models/globals_for_function_path.yaml"), + ("tests/functional/commands/validate/lib/models/globals_for_simpletable.yaml"), + ("tests/functional/commands/validate/lib/models/implicit_and_explicit_api_with_conditions.yaml"), + ("tests/functional/commands/validate/lib/models/implicit_api.yaml"), + ("tests/functional/commands/validate/lib/models/implicit_api_with_auth_and_conditions_max.yaml"), + ("tests/functional/commands/validate/lib/models/implicit_api_with_many_conditions.yaml"), + ("tests/functional/commands/validate/lib/models/implicit_api_with_serverless_rest_api_resource.yaml"), + ("tests/functional/commands/validate/lib/models/intrinsic_functions.yaml"), + ("tests/functional/commands/validate/lib/models/iot_rule.yaml"), + ("tests/functional/commands/validate/lib/models/layers_all_properties.yaml"), + ("tests/functional/commands/validate/lib/models/layers_with_intrinsics.yaml"), + ("tests/functional/commands/validate/lib/models/no_implicit_api_with_serverless_rest_api_resource.yaml"), + ("tests/functional/commands/validate/lib/models/s3.yaml"), + ("tests/functional/commands/validate/lib/models/s3_create_remove.yaml"), + ("tests/functional/commands/validate/lib/models/s3_existing_lambda_notification_configuration.yaml"), + ("tests/functional/commands/validate/lib/models/s3_existing_other_notification_configuration.yaml"), + ("tests/functional/commands/validate/lib/models/s3_filter.yaml"), + ("tests/functional/commands/validate/lib/models/s3_multiple_events_same_bucket.yaml"), + ("tests/functional/commands/validate/lib/models/s3_multiple_functions.yaml"), + ("tests/functional/commands/validate/lib/models/s3_with_condition.yaml"), + ("tests/functional/commands/validate/lib/models/s3_with_dependsOn.yaml"), + ("tests/functional/commands/validate/lib/models/simple_table_ref_parameter_intrinsic.yaml"), + ("tests/functional/commands/validate/lib/models/simple_table_with_extra_tags.yaml"), + ("tests/functional/commands/validate/lib/models/simple_table_with_table_name.yaml"), + ("tests/functional/commands/validate/lib/models/simpletable.yaml"), + ("tests/functional/commands/validate/lib/models/simpletable_with_sse.yaml"), + ("tests/functional/commands/validate/lib/models/sns.yaml"), + ("tests/functional/commands/validate/lib/models/sns_existing_other_subscription.yaml"), + ("tests/functional/commands/validate/lib/models/sns_topic_outside_template.yaml"), + ("tests/functional/commands/validate/lib/models/sqs.yaml"), + ("tests/functional/commands/validate/lib/models/streams.yaml"), + ("tests/functional/commands/validate/lib/models/unsupported_resources.yaml") + ] + def test_valid_template(self): template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -191,4 +310,17 @@ def test_valid_template_with_s3_object_passed(self): # validate the CodeUri was not changed self.assertEquals(validator.sam_template.get("Resources").get("ServerlessApi").get("Properties").get("DefinitionUri"), {"Bucket": "mybucket-name", "Key": "swagger", "Version": 121212}) - self.assertEquals(validator.sam_template.get("Resources").get("ServerlessFunction").get("Properties").get("CodeUri"), {"Bucket": "mybucket-name", "Key": "code.zip", "Version": 121212}) \ No newline at end of file + self.assertEquals(validator.sam_template.get("Resources").get("ServerlessFunction").get("Properties").get("CodeUri"), {"Bucket": "mybucket-name", "Key": "code.zip", "Version": 121212}) + + @parameterized.expand(VALID_TEST_TEMPLATES) + def test_valid_api_request_model_template(self, template_path): + with open(template_path) as f: + template = yamlhelper.yaml_parse(f.read()) + managed_policy_mock = Mock() + managed_policy_mock.load.return_value = {"PolicyName": "FakePolicy"} + + validator = SamTemplateValidator(template, managed_policy_mock) + + # Should not throw an exception + validator.is_valid() + diff --git a/tests/integration/deprecation/__init__.py b/tests/integration/deprecation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/deprecation/test_deprecation_warning.py b/tests/integration/deprecation/test_deprecation_warning.py new file mode 100644 index 0000000000..aebf882f76 --- /dev/null +++ b/tests/integration/deprecation/test_deprecation_warning.py @@ -0,0 +1,31 @@ +import os +import subprocess +import sys + +from unittest import TestCase + +from samcli.cli.command import DEPRECATION_NOTICE + + +class TestPy2DeprecationWarning(TestCase): + + def base_command(self): + command = "sam" + if os.getenv("SAM_CLI_DEV"): + command = "samdev" + + return command + + def run_cmd(self): + # Checking with base command to see if warning is present if running in python2 + cmd_list = [self.base_command()] + process = subprocess.Popen(cmd_list, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return process + + def test_print_deprecation_warning_if_py2(self): + process = self.run_cmd() + (stdoutdata, stderrdata) = process.communicate() + + # Deprecation notice should be part of the command output if running in python 2 + if sys.version_info.major == 2: + self.assertIn(DEPRECATION_NOTICE, stderrdata.decode()) diff --git a/tests/integration/local/invoke/test_integrations_cli.py b/tests/integration/local/invoke/test_integrations_cli.py index 6be7ab7e70..dcd7586b8c 100644 --- a/tests/integration/local/invoke/test_integrations_cli.py +++ b/tests/integration/local/invoke/test_integrations_cli.py @@ -13,11 +13,11 @@ from tests.integration.local.invoke.layer_utils import LayerUtils from .invoke_integ_base import InvokeIntegBase -from tests.testing_utils import IS_WINDOWS, RUNNING_ON_TRAVIS, RUNNING_TEST_FOR_MASTER_ON_TRAVIS +from tests.testing_utils import IS_WINDOWS, RUNNING_ON_CI, RUNNING_TEST_FOR_MASTER_ON_CI # Layers tests require credentials and Travis will only add credentials to the env if the PR is from the same repo. # This is to restrict layers tests to run outside of Travis and when the branch is not master. -SKIP_LAYERS_TESTS = RUNNING_ON_TRAVIS and RUNNING_TEST_FOR_MASTER_ON_TRAVIS +SKIP_LAYERS_TESTS = RUNNING_ON_CI and RUNNING_TEST_FOR_MASTER_ON_CI try: from pathlib import Path @@ -466,6 +466,15 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cls.layer_utils.delete_layers() + # Added to handle the case where ^C failed the test due to invalid cleanup of layers + docker_client = docker.from_env() + samcli_images = docker_client.images.list(name='samcli/lambda') + for image in samcli_images: + docker_client.images.remove(image.id) + integ_layer_cache_dir = Path().home().joinpath("integ_layer_cache") + if integ_layer_cache_dir.exists(): + shutil.rmtree(str(integ_layer_cache_dir)) + super(TestLayerVersion, cls).tearDownClass() @parameterized.expand([ diff --git a/tests/integration/local/start_api/test_start_api.py b/tests/integration/local/start_api/test_start_api.py index 700491260d..724eedcb43 100644 --- a/tests/integration/local/start_api/test_start_api.py +++ b/tests/integration/local/start_api/test_start_api.py @@ -2,6 +2,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from time import time +from samcli.local.apigw.local_apigw_service import Route from .start_api_integ_base import StartApiIntegBaseClass @@ -9,6 +10,7 @@ class TestParallelRequests(StartApiIntegBaseClass): """ Test Class centered around sending parallel requests to the service `sam local start-api` """ + # This is here so the setUpClass doesn't fail. Set to this something else once the class is implemented template_path = "/testdata/start_api/template.yaml" @@ -24,8 +26,10 @@ def test_same_endpoint(self): start_time = time() thread_pool = ThreadPoolExecutor(number_of_requests) - futures = [thread_pool.submit(requests.get, self.url + "/sleepfortenseconds/function1") - for _ in range(0, number_of_requests)] + futures = [ + thread_pool.submit(requests.get, self.url + "/sleepfortenseconds/function1") + for _ in range(0, number_of_requests) + ] results = [r.result() for r in as_completed(futures)] end_time = time() @@ -36,7 +40,9 @@ def test_same_endpoint(self): for result in results: self.assertEquals(result.status_code, 200) - self.assertEquals(result.json(), {"message": "HelloWorld! I just slept and waking up."}) + self.assertEquals( + result.json(), {"message": "HelloWorld! I just slept and waking up."} + ) def test_different_endpoints(self): """ @@ -47,10 +53,18 @@ def test_different_endpoints(self): start_time = time() thread_pool = ThreadPoolExecutor(10) - test_url_paths = ["/sleepfortenseconds/function0", "/sleepfortenseconds/function1"] - - futures = [thread_pool.submit(requests.get, self.url + test_url_paths[function_num % len(test_url_paths)]) - for function_num in range(0, number_of_requests)] + test_url_paths = [ + "/sleepfortenseconds/function0", + "/sleepfortenseconds/function1", + ] + + futures = [ + thread_pool.submit( + requests.get, + self.url + test_url_paths[function_num % len(test_url_paths)], + ) + for function_num in range(0, number_of_requests) + ] results = [r.result() for r in as_completed(futures)] end_time = time() @@ -61,13 +75,16 @@ def test_different_endpoints(self): for result in results: self.assertEquals(result.status_code, 200) - self.assertEquals(result.json(), {"message": "HelloWorld! I just slept and waking up."}) + self.assertEquals( + result.json(), {"message": "HelloWorld! I just slept and waking up."} + ) class TestServiceErrorResponses(StartApiIntegBaseClass): """ Test Class centered around the Error Responses the Service can return for a given api """ + # This is here so the setUpClass doesn't fail. Set to this something else once the class is implemented. template_path = "/testdata/start_api/template.yaml" @@ -100,6 +117,7 @@ class TestService(StartApiIntegBaseClass): """ Testing general requirements around the Service that powers `sam local start-api` """ + template_path = "/testdata/start_api/template.yaml" def setUp(self): @@ -112,7 +130,7 @@ def test_calling_proxy_endpoint(self): response = requests.get(self.url + "/proxypath/this/is/some/path") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_get_call_with_path_setup_with_any_implicit_api(self): """ @@ -121,7 +139,7 @@ def test_get_call_with_path_setup_with_any_implicit_api(self): response = requests.get(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_post_call_with_path_setup_with_any_implicit_api(self): """ @@ -130,7 +148,7 @@ def test_post_call_with_path_setup_with_any_implicit_api(self): response = requests.post(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_put_call_with_path_setup_with_any_implicit_api(self): """ @@ -139,7 +157,7 @@ def test_put_call_with_path_setup_with_any_implicit_api(self): response = requests.put(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_head_call_with_path_setup_with_any_implicit_api(self): """ @@ -156,7 +174,7 @@ def test_delete_call_with_path_setup_with_any_implicit_api(self): response = requests.delete(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_options_call_with_path_setup_with_any_implicit_api(self): """ @@ -173,7 +191,7 @@ def test_patch_call_with_path_setup_with_any_implicit_api(self): response = requests.patch(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) class TestStartApiWithSwaggerApis(StartApiIntegBaseClass): @@ -190,7 +208,7 @@ def test_get_call_with_path_setup_with_any_swagger(self): response = requests.get(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_post_call_with_path_setup_with_any_swagger(self): """ @@ -199,7 +217,7 @@ def test_post_call_with_path_setup_with_any_swagger(self): response = requests.post(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_put_call_with_path_setup_with_any_swagger(self): """ @@ -208,7 +226,7 @@ def test_put_call_with_path_setup_with_any_swagger(self): response = requests.put(self.url + "/anyandall", json={}) self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_head_call_with_path_setup_with_any_swagger(self): """ @@ -225,7 +243,7 @@ def test_delete_call_with_path_setup_with_any_swagger(self): response = requests.delete(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_options_call_with_path_setup_with_any_swagger(self): """ @@ -242,34 +260,148 @@ def test_patch_call_with_path_setup_with_any_swagger(self): response = requests.patch(self.url + "/anyandall") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_function_not_defined_in_template(self): response = requests.get(self.url + "/nofunctionfound") self.assertEquals(response.status_code, 502) - self.assertEquals(response.json(), {"message": "No function defined for resource method"}) + self.assertEquals( + response.json(), {"message": "No function defined for resource method"} + ) def test_function_with_no_api_event_is_reachable(self): response = requests.get(self.url + "/functionwithnoapievent") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_lambda_function_resource_is_reachable(self): response = requests.get(self.url + "/nonserverlessfunction") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_binary_request(self): + """ + This tests that the service can accept and invoke a lambda when given binary data in a request + """ + input_data = self.get_binary_data(self.binary_data_file) + response = requests.post( + self.url + "/echobase64eventbody", + headers={"Content-Type": "image/gif"}, + data=input_data, + ) + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.headers.get("Content-Type"), "image/gif") + self.assertEquals(response.content, input_data) + + def test_binary_response(self): + """ + Binary data is returned correctly + """ + expected = self.get_binary_data(self.binary_data_file) + + response = requests.get(self.url + "/base64response") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.headers.get("Content-Type"), "image/gif") + self.assertEquals(response.content, expected) + + +class TestStartApiWithSwaggerRestApis(StartApiIntegBaseClass): + template_path = "/testdata/start_api/swagger-rest-api-template.yaml" + binary_data_file = "testdata/start_api/binarydata.gif" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_get_call_with_path_setup_with_any_swagger(self): + """ + Get Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.get(self.url + "/anyandall") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_post_call_with_path_setup_with_any_swagger(self): + """ + Post Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.post(self.url + "/anyandall", json={}) + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_put_call_with_path_setup_with_any_swagger(self): + """ + Put Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.put(self.url + "/anyandall", json={}) + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_head_call_with_path_setup_with_any_swagger(self): + """ + Head Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.head(self.url + "/anyandall") + + self.assertEquals(response.status_code, 200) + + def test_delete_call_with_path_setup_with_any_swagger(self): + """ + Delete Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.delete(self.url + "/anyandall") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_options_call_with_path_setup_with_any_swagger(self): + """ + Options Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.options(self.url + "/anyandall") + + self.assertEquals(response.status_code, 200) + + def test_patch_call_with_path_setup_with_any_swagger(self): + """ + Patch Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.patch(self.url + "/anyandall") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_function_not_defined_in_template(self): + response = requests.get(self.url + "/nofunctionfound") + + self.assertEquals(response.status_code, 502) + self.assertEquals( + response.json(), {"message": "No function defined for resource method"} + ) + + def test_lambda_function_resource_is_reachable(self): + response = requests.get(self.url + "/nonserverlessfunction") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) def test_binary_request(self): """ This tests that the service can accept and invoke a lambda when given binary data in a request """ input_data = self.get_binary_data(self.binary_data_file) - response = requests.post(self.url + '/echobase64eventbody', - headers={"Content-Type": "image/gif"}, - data=input_data) + response = requests.post( + self.url + "/echobase64eventbody", + headers={"Content-Type": "image/gif"}, + data=input_data, + ) self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -281,7 +413,7 @@ def test_binary_response(self): """ expected = self.get_binary_data(self.binary_data_file) - response = requests.get(self.url + '/base64response') + response = requests.get(self.url + "/base64response") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -292,6 +424,7 @@ class TestServiceResponses(StartApiIntegBaseClass): """ Test Class centered around the different responses that can happen in Lambda and pass through start-api """ + template_path = "/testdata/start_api/template.yaml" binary_data_file = "testdata/start_api/binarydata.gif" @@ -303,14 +436,16 @@ def test_multiple_headers_response(self): self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "text/plain") - self.assertEquals(response.headers.get("MyCustomHeader"), 'Value1, Value2') + self.assertEquals(response.headers.get("MyCustomHeader"), "Value1, Value2") def test_multiple_headers_overrides_headers_response(self): response = requests.get(self.url + "/multipleheadersoverridesheaders") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "text/plain") - self.assertEquals(response.headers.get("MyCustomHeader"), 'Value1, Value2, Custom') + self.assertEquals( + response.headers.get("MyCustomHeader"), "Value1, Value2, Custom" + ) def test_binary_response(self): """ @@ -318,7 +453,7 @@ def test_binary_response(self): """ expected = self.get_binary_data(self.binary_data_file) - response = requests.get(self.url + '/base64response') + response = requests.get(self.url + "/base64response") self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -331,7 +466,7 @@ def test_default_header_content_type(self): response = requests.get(self.url + "/onlysetstatuscode") self.assertEquals(response.status_code, 200) - self.assertEquals(response.content.decode('utf-8'), "no data") + self.assertEquals(response.content.decode("utf-8"), "no data") self.assertEquals(response.headers.get("Content-Type"), "application/json") def test_default_status_code(self): @@ -342,7 +477,7 @@ def test_default_status_code(self): response = requests.get(self.url + "/onlysetbody") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_string_status_code(self): """ @@ -359,25 +494,32 @@ def test_default_body(self): response = requests.get(self.url + "/onlysetstatuscode") self.assertEquals(response.status_code, 200) - self.assertEquals(response.content.decode('utf-8'), "no data") + self.assertEquals(response.content.decode("utf-8"), "no data") def test_function_writing_to_stdout(self): response = requests.get(self.url + "/writetostdout") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) def test_function_writing_to_stderr(self): response = requests.get(self.url + "/writetostderr") self.assertEquals(response.status_code, 200) - self.assertEquals(response.json(), {'hello': 'world'}) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_integer_body(self): + response = requests.get(self.url + "/echo_integer_body") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.content.decode("utf-8"), "42") class TestServiceRequests(StartApiIntegBaseClass): """ Test Class centered around the different requests that can happen """ + template_path = "/testdata/start_api/template.yaml" binary_data_file = "testdata/start_api/binarydata.gif" @@ -389,9 +531,11 @@ def test_binary_request(self): This tests that the service can accept and invoke a lambda when given binary data in a request """ input_data = self.get_binary_data(self.binary_data_file) - response = requests.post(self.url + '/echobase64eventbody', - headers={"Content-Type": "image/gif"}, - data=input_data) + response = requests.post( + self.url + "/echobase64eventbody", + headers={"Content-Type": "image/gif"}, + data=input_data, + ) self.assertEquals(response.status_code, 200) self.assertEquals(response.headers.get("Content-Type"), "image/gif") @@ -401,15 +545,20 @@ def test_request_with_form_data(self): """ Form-encoded data should be put into the Event to Lambda """ - response = requests.post(self.url + "/echoeventbody", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data='key=value') + response = requests.post( + self.url + "/echoeventbody", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data="key=value", + ) self.assertEquals(response.status_code, 200) response_data = response.json() - self.assertEquals(response_data.get("headers").get("Content-Type"), "application/x-www-form-urlencoded") + self.assertEquals( + response_data.get("headers").get("Content-Type"), + "application/x-www-form-urlencoded", + ) self.assertEquals(response_data.get("body"), "key=value") def test_request_to_an_endpoint_with_two_different_handlers(self): @@ -419,46 +568,55 @@ def test_request_to_an_endpoint_with_two_different_handlers(self): response_data = response.json() - self.assertEquals(response_data.get("handler"), 'echo_event_handler_2') + self.assertEquals(response_data.get("handler"), "echo_event_handler_2") def test_request_with_multi_value_headers(self): - response = requests.get(self.url + "/echoeventbody", - headers={"Content-Type": "application/x-www-form-urlencoded, image/gif"}) + response = requests.get( + self.url + "/echoeventbody", + headers={"Content-Type": "application/x-www-form-urlencoded, image/gif"}, + ) self.assertEquals(response.status_code, 200) response_data = response.json() - self.assertEquals(response_data.get("multiValueHeaders").get("Content-Type"), - ["application/x-www-form-urlencoded, image/gif"]) - self.assertEquals(response_data.get("headers").get("Content-Type"), - "application/x-www-form-urlencoded, image/gif") + self.assertEquals( + response_data.get("multiValueHeaders").get("Content-Type"), + ["application/x-www-form-urlencoded, image/gif"], + ) + self.assertEquals( + response_data.get("headers").get("Content-Type"), + "application/x-www-form-urlencoded, image/gif", + ) def test_request_with_query_params(self): """ Query params given should be put into the Event to Lambda """ - response = requests.get(self.url + "/id/4", - params={"key": "value"}) + response = requests.get(self.url + "/id/4", params={"key": "value"}) self.assertEquals(response.status_code, 200) response_data = response.json() self.assertEquals(response_data.get("queryStringParameters"), {"key": "value"}) - self.assertEquals(response_data.get("multiValueQueryStringParameters"), {"key": ["value"]}) + self.assertEquals( + response_data.get("multiValueQueryStringParameters"), {"key": ["value"]} + ) def test_request_with_list_of_query_params(self): """ Query params given should be put into the Event to Lambda """ - response = requests.get(self.url + "/id/4", - params={"key": ["value", "value2"]}) + response = requests.get(self.url + "/id/4", params={"key": ["value", "value2"]}) self.assertEquals(response.status_code, 200) response_data = response.json() self.assertEquals(response_data.get("queryStringParameters"), {"key": "value2"}) - self.assertEquals(response_data.get("multiValueQueryStringParameters"), {"key": ["value", "value2"]}) + self.assertEquals( + response_data.get("multiValueQueryStringParameters"), + {"key": ["value", "value2"]}, + ) def test_request_with_path_params(self): """ @@ -482,7 +640,9 @@ def test_request_with_many_path_params(self): response_data = response.json() - self.assertEquals(response_data.get("pathParameters"), {"id": "4", "user": "jacob"}) + self.assertEquals( + response_data.get("pathParameters"), {"id": "4", "user": "jacob"} + ) def test_forward_headers_are_added_to_event(self): """ @@ -493,15 +653,22 @@ def test_forward_headers_are_added_to_event(self): response_data = response.json() self.assertEquals(response_data.get("headers").get("X-Forwarded-Proto"), "http") - self.assertEquals(response_data.get("multiValueHeaders").get("X-Forwarded-Proto"), ["http"]) - self.assertEquals(response_data.get("headers").get("X-Forwarded-Port"), self.port) - self.assertEquals(response_data.get("multiValueHeaders").get("X-Forwarded-Port"), [self.port]) + self.assertEquals( + response_data.get("multiValueHeaders").get("X-Forwarded-Proto"), ["http"] + ) + self.assertEquals( + response_data.get("headers").get("X-Forwarded-Port"), self.port + ) + self.assertEquals( + response_data.get("multiValueHeaders").get("X-Forwarded-Port"), [self.port] + ) class TestStartApiWithStage(StartApiIntegBaseClass): """ Test Class centered around the different responses that can happen in Lambda and pass through start-api """ + template_path = "/testdata/start_api/template.yaml" def setUp(self): @@ -523,13 +690,14 @@ def test_global_stage_variables(self): response_data = response.json() - self.assertEquals(response_data.get("stageVariables"), {'VarName': 'varValue'}) + self.assertEquals(response_data.get("stageVariables"), {"VarName": "varValue"}) class TestStartApiWithStageAndSwagger(StartApiIntegBaseClass): """ Test Class centered around the different responses that can happen in Lambda and pass through start-api """ + template_path = "/testdata/start_api/swagger-template.yaml" def setUp(self): @@ -549,4 +717,253 @@ def test_swagger_stage_variable(self): self.assertEquals(response.status_code, 200) response_data = response.json() - self.assertEquals(response_data.get("stageVariables"), {'VarName': 'varValue'}) + self.assertEquals(response_data.get("stageVariables"), {"VarName": "varValue"}) + + +class TestServiceCorsSwaggerRequests(StartApiIntegBaseClass): + """ + Test to check that the correct headers are being added with Cors with swagger code + """ + + template_path = "/testdata/start_api/swagger-template.yaml" + binary_data_file = "testdata/start_api/binarydata.gif" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_cors_swagger_options(self): + """ + This tests that the Cors are added to option requests in the swagger template + """ + response = requests.options(self.url + "/echobase64eventbody") + + self.assertEquals(response.status_code, 200) + + self.assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*") + self.assertEquals( + response.headers.get("Access-Control-Allow-Headers"), + "origin, x-requested-with", + ) + self.assertEquals( + response.headers.get("Access-Control-Allow-Methods"), "GET,OPTIONS" + ) + self.assertEquals(response.headers.get("Access-Control-Max-Age"), "510") + + +class TestServiceCorsGlobalRequests(StartApiIntegBaseClass): + """ + Test to check that the correct headers are being added with Cors with the global property + """ + + template_path = "/testdata/start_api/template.yaml" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_cors_global(self): + """ + This tests that the Cors are added to options requests when the global property is set + """ + response = requests.options(self.url + "/echobase64eventbody") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*") + self.assertEquals(response.headers.get("Access-Control-Allow-Headers"), None) + self.assertEquals( + response.headers.get("Access-Control-Allow-Methods"), + ",".join(sorted(Route.ANY_HTTP_METHODS)), + ) + self.assertEquals(response.headers.get("Access-Control-Max-Age"), None) + + def test_cors_global_get(self): + """ + This tests that the Cors are added to post requests when the global property is set + """ + response = requests.get(self.url + "/onlysetstatuscode") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.content.decode("utf-8"), "no data") + self.assertEquals(response.headers.get("Content-Type"), "application/json") + self.assertEquals(response.headers.get("Access-Control-Allow-Origin"), None) + self.assertEquals(response.headers.get("Access-Control-Allow-Headers"), None) + self.assertEquals(response.headers.get("Access-Control-Allow-Methods"), None) + self.assertEquals(response.headers.get("Access-Control-Max-Age"), None) + + +class TestStartApiWithCloudFormationStage(StartApiIntegBaseClass): + """ + Test Class centered around the different responses that can happen in Lambda and pass through start-api + """ + + template_path = "/testdata/start_api/swagger-rest-api-template.yaml" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_default_stage_name(self): + response = requests.get(self.url + "/echoeventbody") + + self.assertEquals(response.status_code, 200) + + response_data = response.json() + self.assertEquals(response_data.get("requestContext", {}).get("stage"), "Dev") + + def test_global_stage_variables(self): + response = requests.get(self.url + "/echoeventbody") + + self.assertEquals(response.status_code, 200) + + response_data = response.json() + + self.assertEquals(response_data.get("stageVariables"), {"Stack": "Dev"}) + + +class TestStartApiWithMethodsAndResources(StartApiIntegBaseClass): + template_path = "/testdata/start_api/methods-resources-api-template.yaml" + binary_data_file = "testdata/start_api/binarydata.gif" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_get_call_with_path_setup_with_any_swagger(self): + """ + Get Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.get(self.url + "/root/anyandall") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_post_call_with_path_setup_with_any_swagger(self): + """ + Post Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.post(self.url + "/root/anyandall", json={}) + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_put_call_with_path_setup_with_any_swagger(self): + """ + Put Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.put(self.url + "/root/anyandall", json={}) + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_head_call_with_path_setup_with_any_swagger(self): + """ + Head Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.head(self.url + "/root/anyandall") + + self.assertEquals(response.status_code, 200) + + def test_delete_call_with_path_setup_with_any_swagger(self): + """ + Delete Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.delete(self.url + "/root/anyandall") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_options_call_with_path_setup_with_any_swagger(self): + """ + Options Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.options(self.url + "/root/anyandall") + + self.assertEquals(response.status_code, 200) + + def test_patch_call_with_path_setup_with_any_swagger(self): + """ + Patch Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.patch(self.url + "/root/anyandall") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_function_not_defined_in_template(self): + response = requests.get(self.url + "/root/nofunctionfound") + + self.assertEquals(response.status_code, 502) + self.assertEquals( + response.json(), {"message": "No function defined for resource method"} + ) + + def test_lambda_function_resource_is_reachable(self): + response = requests.get(self.url + "/root/nonserverlessfunction") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + def test_binary_request(self): + """ + This tests that the service can accept and invoke a lambda when given binary data in a request + """ + input_data = self.get_binary_data(self.binary_data_file) + response = requests.post( + self.url + "/root/echobase64eventbody", + headers={"Content-Type": "image/gif"}, + data=input_data, + ) + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.headers.get("Content-Type"), "image/gif") + self.assertEquals(response.content, input_data) + + def test_binary_response(self): + """ + Binary data is returned correctly + """ + expected = self.get_binary_data(self.binary_data_file) + + response = requests.get(self.url + "/root/base64response") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.headers.get("Content-Type"), "image/gif") + self.assertEquals(response.content, expected) + + def test_proxy_response(self): + """ + Binary data is returned correctly + """ + response = requests.get(self.url + "/root/v1/test") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {"hello": "world"}) + + +class TestCDKApiGateway(StartApiIntegBaseClass): + template_path = "/testdata/start_api/cdk-sample-output.yaml" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_get_with_cdk(self): + """ + Get Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.get(self.url + "/hello-world") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {'hello': 'world'}) + + +class TestServerlessApiGateway(StartApiIntegBaseClass): + template_path = "/testdata/start_api/serverless-sample-output.yaml" + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + + def test_get_with_serverless(self): + """ + Get Request to a path that was defined as ANY in SAM through Swagger + """ + response = requests.get(self.url + "/hello-world") + + self.assertEquals(response.status_code, 200) + self.assertEquals(response.json(), {'hello': 'world'}) diff --git a/tests/integration/publish/test_command_integ.py b/tests/integration/publish/test_command_integ.py index b724d2c8bc..37531fe631 100644 --- a/tests/integration/publish/test_command_integ.py +++ b/tests/integration/publish/test_command_integ.py @@ -1,4 +1,3 @@ -import os import re import json from subprocess import Popen, PIPE @@ -7,13 +6,14 @@ from samcli.commands.publish.command import SEMANTIC_VERSION from .publish_app_integ_base import PublishAppIntegBase +from tests.testing_utils import RUNNING_ON_CI, RUNNING_TEST_FOR_MASTER_ON_CI -# Publish tests require credentials and Travis will only add credentials to the env if the PR is from the same repo. -# This is to restrict publish tests to run outside of Travis and when the branch is not master. -SKIP_PUBLISH_TESTS = os.environ.get("TRAVIS", False) and os.environ.get("TRAVIS_BRANCH", "master") != "master" +# Publish tests require credentials and CI/CD will only add credentials to the env if the PR is from the same repo. +# This is to restrict publish tests to run outside of CI/CD and when the branch is not master. +SKIP_PUBLISH_TESTS = RUNNING_ON_CI and RUNNING_TEST_FOR_MASTER_ON_CI -@skipIf(SKIP_PUBLISH_TESTS, "Skip publish tests in Travis only") +@skipIf(SKIP_PUBLISH_TESTS, "Skip publish tests in CI/CD only") class TestPublishExistingApp(PublishAppIntegBase): def setUp(self): @@ -78,7 +78,7 @@ def test_create_application_version_with_semantic_version_option(self): self.assert_metadata_details(app_metadata, process_stdout.decode('utf-8')) -@skipIf(SKIP_PUBLISH_TESTS, "Skip publish tests in Travis only") +@skipIf(SKIP_PUBLISH_TESTS, "Skip publish tests in CI/CD only") class TestPublishNewApp(PublishAppIntegBase): def setUp(self): diff --git a/tests/integration/telemetry/integ_base.py b/tests/integration/telemetry/integ_base.py index 4acb1b661c..c8d0df5b60 100644 --- a/tests/integration/telemetry/integ_base.py +++ b/tests/integration/telemetry/integ_base.py @@ -60,7 +60,7 @@ def run_cmd(self, stdin_data="", optout_envvar_value=None): env = os.environ.copy() - # remove the envvar which usually is set in Travis. This interferes with tests + # remove the envvar which usually is set in CI/CD. This interferes with tests env.pop("SAM_CLI_TELEMETRY", None) if optout_envvar_value: # But if the caller explicitly asked us to opt-out via EnvVar, then set it here diff --git a/tests/integration/testdata/start_api/cdk-sample-output.yaml b/tests/integration/testdata/start_api/cdk-sample-output.yaml new file mode 100644 index 0000000000..203ed94906 --- /dev/null +++ b/tests/integration/testdata/start_api/cdk-sample-output.yaml @@ -0,0 +1,276 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Resources: + HelloHandlerServiceRole11EF7C63: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ServiceRole/Resource + + HelloHandler2E4FBA4D: + Type: AWS::Lambda::Function + Properties: + Code: '.' + Handler: main.handler + Runtime: python3.6 + + HelloHandlerApiPermissionANYAC4E141E: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: HelloHandler2E4FBA4D + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: EndpointEEF1FD8F + - / + - Ref: EndpointDeploymentStageprodB78BEEA0 + - /*/ + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ApiPermission.ANY.. + HelloHandlerApiPermissionTestANYDDD56D72: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: HelloHandler2E4FBA4D + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: EndpointEEF1FD8F + - /test-invoke-stage/*/ + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ApiPermission.Test.ANY.. + HelloHandlerApiPermissionANYproxy90E90CD6: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: HelloHandler2E4FBA4D + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: EndpointEEF1FD8F + - / + - Ref: EndpointDeploymentStageprodB78BEEA0 + - /*/{proxy+} + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ApiPermission.ANY..{proxy+} + HelloHandlerApiPermissionTestANYproxy9803526C: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: + Ref: HelloHandler2E4FBA4D + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: EndpointEEF1FD8F + - /test-invoke-stage/*/{proxy+} + Metadata: + aws:cdk:path: CdkWorkshopStack/HelloHandler/ApiPermission.Test.ANY..{proxy+} + + EndpointEEF1FD8F: + Type: AWS::ApiGateway::RestApi + Properties: + Name: Endpoint + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/Resource + + EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf: + Type: AWS::ApiGateway::Deployment + Properties: + RestApiId: + Ref: EndpointEEF1FD8F + Description: Automatically created by the RestApi construct + DependsOn: + - Endpointproxy39E2174E + - EndpointANY485C938B + - EndpointproxyANYC09721C5 + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/Deployment/Resource + + + EndpointDeploymentStageprodB78BEEA0: + Type: AWS::ApiGateway::Stage + Properties: + RestApiId: + Ref: EndpointEEF1FD8F + DeploymentId: + Ref: EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf + StageName: prod + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/DeploymentStage.prod/Resource + + EndpointCloudWatchRoleC3C64E0F: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: apigateway.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/CloudWatchRole/Resource + + + EndpointAccountB8304247: + Type: AWS::ApiGateway::Account + Properties: + CloudWatchRoleArn: + Fn::GetAtt: + - EndpointCloudWatchRoleC3C64E0F + - Arn + DependsOn: + - EndpointEEF1FD8F + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/Account + + Endpointproxy39E2174E: + Type: AWS::ApiGateway::Resource + Properties: + ParentId: + Fn::GetAtt: + - EndpointEEF1FD8F + - RootResourceId + PathPart: "{proxy+}" + RestApiId: + Ref: EndpointEEF1FD8F + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/{proxy+}/Resource + EndpointproxyANYC09721C5: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: ANY + ResourceId: + Ref: Endpointproxy39E2174E + RestApiId: + Ref: EndpointEEF1FD8F + AuthorizationType: NONE + Integration: + IntegrationHttpMethod: POST + Type: AWS_PROXY + Uri: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":apigateway:" + - Ref: AWS::Region + - :lambda:path/2015-03-31/functions/ + - Fn::GetAtt: + - HelloHandler2E4FBA4D + - Arn + - /invocations + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/{proxy+}/ANY/Resource + EndpointANY485C938B: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: ANY + ResourceId: + Fn::GetAtt: + - EndpointEEF1FD8F + - RootResourceId + RestApiId: + Ref: EndpointEEF1FD8F + AuthorizationType: NONE + Integration: + IntegrationHttpMethod: POST + Type: AWS_PROXY + Uri: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":apigateway:" + - Ref: AWS::Region + - :lambda:path/2015-03-31/functions/ + - Fn::GetAtt: + - HelloHandler2E4FBA4D + - Arn + - /invocations + Metadata: + aws:cdk:path: CdkWorkshopStack/Endpoint/ANY/Resource + CDKMetadata: + Type: AWS::CDK::Metadata + Properties: + Modules: aws-cdk=0.22.0,jsii-runtime=node.js/v12.4.0 +Parameters: + HelloHandlerCodeS3Bucket4359A483: + Type: String + Description: S3 bucket for asset "CdkWorkshopStack/HelloHandler/Code" + HelloHandlerCodeS3VersionKey07D12610: + Type: String + Description: S3 key for asset version "CdkWorkshopStack/HelloHandler/Code" +Outputs: + Endpoint8024A810: + Value: + Fn::Join: + - "" + - - https:// + - Ref: EndpointEEF1FD8F + - .execute-api. + - Ref: AWS::Region + - "." + - Ref: AWS::URLSuffix + - / + - Ref: EndpointDeploymentStageprodB78BEEA0 + - / + Export: + Name: CdkWorkshopStack:Endpoint8024A810 + diff --git a/tests/integration/testdata/start_api/main.py b/tests/integration/testdata/start_api/main.py index 7e3875cdfc..fc6bc06482 100644 --- a/tests/integration/testdata/start_api/main.py +++ b/tests/integration/testdata/start_api/main.py @@ -4,39 +4,36 @@ def handler(event, context): - return {"statusCode": 200, "body": json.dumps({"hello": "world"})} def echo_event_handler(event, context): - return {"statusCode": 200, "body": json.dumps(event)} def echo_event_handler_2(event, context): - event['handler'] = 'echo_event_handler_2' return {"statusCode": 200, "body": json.dumps(event)} -def content_type_setter_handler(event, context): +def echo_integer_body(event, context): + return {"statusCode": 200, "body": 42} + +def content_type_setter_handler(event, context): return {"statusCode": 200, "body": "hello", "headers": {"Content-Type": "text/plain"}} def only_set_status_code_handler(event, context): - return {"statusCode": 200} 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"})} diff --git a/tests/integration/testdata/start_api/methods-resources-api-template.yaml b/tests/integration/testdata/start_api/methods-resources-api-template.yaml new file mode 100644 index 0000000000..9c9fc662a4 --- /dev/null +++ b/tests/integration/testdata/start_api/methods-resources-api-template.yaml @@ -0,0 +1,147 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Resources: + Base64ResponseFunction: + Properties: + Code: "." + Handler: main.base64_response + Runtime: python3.6 + Type: AWS::Lambda::Function + EchoBase64EventBodyFunction: + Properties: + Code: "." + Handler: main.echo_base64_event_body + Runtime: python3.6 + Type: AWS::Lambda::Function + + Dev: + Type: AWS::ApiGateway::Stage + Properties: + StageName: Dev + RestApiId: !Ref TestApi + Variables: + Stack: Dev + + MyNonServerlessLambdaFunction: + Properties: + Code: "." + Handler: main.handler + Runtime: python3.6 + Type: AWS::Lambda::Function + + TestApi: + Type: "AWS::ApiGateway::RestApi" + + + RootApiResource: + Type: "AWS::ApiGateway::Resource" + Properties: + PathPart: "root" + ResourceId: "TestApi" + + AnyAndAllResource: + Type: "AWS::ApiGateway::Resource" + Properties: + PathPart: "anyandall" + ParentId: "RootApiResource" + ResourceId: "TestApi" + AnyAndAllMethod: + Type: "AWS::ApiGateway::Method" + Properties: + HttpMethod: "ANY" + Integration: + Uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyNonServerlessLambdaFunction.Arn}/invocations + ResourceId: "AnyAndAllResource" + RestApiId: "TestApi" + + Base64ResponseResource: + Type: "AWS::ApiGateway::Resource" + Properties: + PathPart: "base64response" + ParentId: "RootApiResource" + ResourceId: "TestApi" + Base64ResponseMethod: + Type: "AWS::ApiGateway::Method" + Properties: + HttpMethod: "GET" + Integration: + Uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Base64ResponseFunction.Arn}/invocations + ContentType: "image~1gif" + ContentHandling: "CONVERT_TO_BINARY" + ResourceId: "Base64ResponseResource" + RestApiId: "TestApi" + + + EchoBase64ResponseResource: + Type: "AWS::ApiGateway::Resource" + Properties: + PathPart: "echobase64eventbody" + ParentId: "RootApiResource" + ResourceId: "TestApi" + EchoBase64ResponseMethod: + Type: "AWS::ApiGateway::Method" + Properties: + HttpMethod: "POST" + Integration: + Uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EchoBase64EventBodyFunction.Arn}/invocations + ContentType: "image~1gif" + ContentHandling: "CONVERT_TO_BINARY" + ResourceId: "EchoBase64ResponseResource" + RestApiId: "TestApi" + + ProxyResource: + Type: "AWS::ApiGateway::Resource" + Properties: + PathPart: "{proxy+}" + ParentId: "v1resource" + ResourceId: "TestApi" + v1resource: + Type: "AWS::ApiGateway::Resource" + Properties: + PathPart: "v1" + ParentId: "RootApiResource" + ResourceId: "TestApi" + ProxyMethod: + Type: "AWS::ApiGateway::Method" + Properties: + HttpMethod: "GET" + Integration: + Uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyNonServerlessLambdaFunction.Arn}/invocations + ResourceId: "ProxyResource" + RestApiId: "TestApi" + + NoFunctionFoundResource: + Type: "AWS::ApiGateway::Resource" + Properties: + PathPart: "nofunctionfound" + ParentId: "RootApiResource" + ResourceId: "TestApi" + NoFunctionFoundMethod: + Type: "AWS::ApiGateway::Method" + Properties: + HttpMethod: "GET" + Integration: + Uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${WhatFunction.Arn}/invocations + ResourceId: "NoFunctionFoundResource" + RestApiId: "TestApi" + + NoServerlessFunctionResource: + Type: "AWS::ApiGateway::Resource" + Properties: + PathPart: "nonserverlessfunction" + ParentId: "RootApiResource" + ResourceId: "TestApi" + NoServerlessFunctionMethod: + Type: "AWS::ApiGateway::Method" + Properties: + HttpMethod: "GET" + Integration: + Uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyNonServerlessLambdaFunction.Arn}/invocations + ResourceId: "NoServerlessFunctionResource" + RestApiId: "TestApi" diff --git a/tests/integration/testdata/start_api/serverless-sample-output.yaml b/tests/integration/testdata/start_api/serverless-sample-output.yaml new file mode 100644 index 0000000000..e181ab7de2 --- /dev/null +++ b/tests/integration/testdata/start_api/serverless-sample-output.yaml @@ -0,0 +1,206 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Description: The AWS CloudFormation template for this Serverless application +Resources: + ServerlessDeploymentBucket: + Type: AWS::S3::Bucket + Properties: + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + HelloWorldLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: "/aws/lambda/serverless-hello-world-dev-helloWorld" + IamRoleLambdaExecution: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: + Fn::Join: + - "-" + - - dev + - serverless-hello-world + - lambda + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + Resource: + - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/serverless-hello-world-dev*:* + - Effect: Allow + Action: + - logs:PutLogEvents + Resource: + - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/serverless-hello-world-dev*:*:* + Path: "/" + RoleName: + Fn::Join: + - "-" + - - serverless-hello-world + - dev + - Ref: AWS::Region + - lambdaRole + HelloWorldLambdaFunction: + Type: AWS::Lambda::Function + Properties: + Code: "." + FunctionName: serverless-hello-world-dev-helloWorld + Handler: main.handler + MemorySize: 1024 + Role: + Fn::GetAtt: + - IamRoleLambdaExecution + - Arn + Runtime: python3.6 + DependsOn: + - HelloWorldLogGroup + - IamRoleLambdaExecution + HelloWorldLambdaVersionkbuu03utDK7jANXe4ADsn4Jcw0Gci6s02eSd52Kg: + Type: AWS::Lambda::Version + DeletionPolicy: Retain + Properties: + FunctionName: + Ref: HelloWorldLambdaFunction + CodeSha256: 2huiVVXNNgaCeFoyZScNWyGKnIMkvxfLD5+hjaVF6sM= + ApiGatewayRestApi: + Type: AWS::ApiGateway::RestApi + Properties: + Name: dev-serverless-hello-world + EndpointConfiguration: + Types: + - EDGE + ApiGatewayResourceHelloDashworld: + Type: AWS::ApiGateway::Resource + Properties: + ParentId: + Fn::GetAtt: + - ApiGatewayRestApi + - RootResourceId + PathPart: hello-world + RestApiId: + Ref: ApiGatewayRestApi + ApiGatewayMethodHelloDashworldOptions: + Type: AWS::ApiGateway::Method + Properties: + AuthorizationType: NONE + HttpMethod: OPTIONS + MethodResponses: + - StatusCode: '200' + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: true + method.response.header.Access-Control-Allow-Headers: true + method.response.header.Access-Control-Allow-Methods: true + method.response.header.Access-Control-Allow-Credentials: true + ResponseModels: {} + RequestParameters: {} + Integration: + Type: MOCK + RequestTemplates: + application/json: "{statusCode:200}" + ContentHandling: CONVERT_TO_TEXT + IntegrationResponses: + - StatusCode: '200' + ResponseParameters: + method.response.header.Access-Control-Allow-Origin: "'*'" + method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'" + method.response.header.Access-Control-Allow-Methods: "'OPTIONS,GET'" + method.response.header.Access-Control-Allow-Credentials: "'false'" + ResponseTemplates: + application/json: |- + #set($origin = $input.params("Origin")) + #if($origin == "") #set($origin = $input.params("origin")) #end + #if($origin.matches(".*")) #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin) #end + ResourceId: + Ref: ApiGatewayResourceHelloDashworld + RestApiId: + Ref: ApiGatewayRestApi + ApiGatewayMethodHelloDashworldGet: + Type: AWS::ApiGateway::Method + Properties: + HttpMethod: GET + RequestParameters: {} + ResourceId: + Ref: ApiGatewayResourceHelloDashworld + RestApiId: + Ref: ApiGatewayRestApi + ApiKeyRequired: false + AuthorizationType: NONE + Integration: + IntegrationHttpMethod: POST + Type: AWS_PROXY + Uri: + Fn::Join: + - '' + - - 'arn:' + - Ref: AWS::Partition + - ":apigateway:" + - Ref: AWS::Region + - ":lambda:path/2015-03-31/functions/" + - Fn::GetAtt: + - HelloWorldLambdaFunction + - Arn + - "/invocations" + MethodResponses: [] + ApiGatewayDeployment1561844009303: + Type: AWS::ApiGateway::Deployment + Properties: + RestApiId: + Ref: ApiGatewayRestApi + StageName: dev + DependsOn: + - ApiGatewayMethodHelloDashworldOptions + - ApiGatewayMethodHelloDashworldGet + HelloWorldLambdaPermissionApiGateway: + Type: AWS::Lambda::Permission + Properties: + FunctionName: + Fn::GetAtt: + - HelloWorldLambdaFunction + - Arn + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: + Fn::Join: + - '' + - - 'arn:' + - Ref: AWS::Partition + - ":execute-api:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - ":" + - Ref: ApiGatewayRestApi + - "/*/*" +Outputs: + ServerlessDeploymentBucketName: + Value: + Ref: ServerlessDeploymentBucket + HelloWorldLambdaFunctionQualifiedArn: + Description: Current Lambda function version + Value: + Ref: HelloWorldLambdaVersionkbuu03utDK7jANXe4ADsn4Jcw0Gci6s02eSd52Kg + ServiceEndpoint: + Description: URL of the service endpoint + Value: + Fn::Join: + - '' + - - https:// + - Ref: ApiGatewayRestApi + - ".execute-api." + - Ref: AWS::Region + - "." + - Ref: AWS::URLSuffix + - "/dev" \ No newline at end of file diff --git a/tests/integration/testdata/start_api/swagger-rest-api-template.yaml b/tests/integration/testdata/start_api/swagger-rest-api-template.yaml new file mode 100644 index 0000000000..5e7be3a95e --- /dev/null +++ b/tests/integration/testdata/start_api/swagger-rest-api-template.yaml @@ -0,0 +1,89 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Resources: + Base64ResponseFunction: + Properties: + Code: "." + Handler: main.base64_response + Runtime: python3.6 + Type: AWS::Lambda::Function + EchoBase64EventBodyFunction: + Properties: + Code: "." + Handler: main.echo_base64_event_body + Runtime: python3.6 + Type: AWS::Lambda::Function + EchoEventBodyFunction: + Properties: + Code: "." + Handler: main.echo_event_handler + Runtime: python3.6 + Type: AWS::Lambda::Function + MyApi: + Properties: + Body: + info: + title: + Ref: AWS::StackName + paths: + "/anyandall": + x-amazon-apigateway-any-method: + x-amazon-apigateway-integration: + httpMethod: POST + responses: {} + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyNonServerlessLambdaFunction.Arn}/invocations + "/base64response": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Base64ResponseFunction.Arn}/invocations + "/echoeventbody": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EchoEventBodyFunction.Arn}/invocations + "/echobase64eventbody": + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EchoBase64EventBodyFunction.Arn}/invocations + "/nofunctionfound": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${WhatFunction.Arn}/invocations + "/nonserverlessfunction": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyNonServerlessLambdaFunction.Arn}/invocations + swagger: '2.0' + x-amazon-apigateway-binary-media-types: + - image/gif + StageName: prod + Type: AWS::ApiGateway::RestApi + Dev: + Type: AWS::ApiGateway::Stage + Properties: + StageName: Dev + RestApiId: MyApi + Variables: + Stack: Dev + MyNonServerlessLambdaFunction: + Properties: + Code: "." + Handler: main.handler + Runtime: python3.6 + Type: AWS::Lambda::Function diff --git a/tests/integration/testdata/start_api/swagger-template.yaml b/tests/integration/testdata/start_api/swagger-template.yaml index 9f987c0d8c..cff33b1f43 100644 --- a/tests/integration/testdata/start_api/swagger-template.yaml +++ b/tests/integration/testdata/start_api/swagger-template.yaml @@ -1,4 +1,4 @@ -AWSTemplateFormatVersion : '2010-09-09' +AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Globals: @@ -14,6 +14,11 @@ Resources: StageName: dev Variables: VarName: varValue + Cors: + AllowOrigin: "*" + AllowMethods: "GET" + AllowHeaders: "origin, x-requested-with" + MaxAge: 510 DefinitionBody: swagger: "2.0" info: diff --git a/tests/integration/testdata/start_api/template.yaml b/tests/integration/testdata/start_api/template.yaml index d9786fb52f..ec0b65978c 100644 --- a/tests/integration/testdata/start_api/template.yaml +++ b/tests/integration/testdata/start_api/template.yaml @@ -9,6 +9,7 @@ Globals: - image~1png Variables: VarName: varValue + Cors: "*" Resources: HelloWorldFunction: Type: AWS::Serverless::Function @@ -73,6 +74,19 @@ Resources: Method: GET Path: /echoeventbody + EchoIntegerBodyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.echo_integer_body + Runtime: python3.6 + CodeUri: . + Events: + EchoEventBodyPath: + Type: Api + Properties: + Method: GET + Path: /echo_integer_body + ContentTypeSetterFunction: Type: AWS::Serverless::Function Properties: diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 3724c94306..1304ee156c 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -2,5 +2,5 @@ import platform IS_WINDOWS = platform.system().lower() == 'windows' -RUNNING_ON_TRAVIS = os.environ.get("TRAVIS", False) -RUNNING_TEST_FOR_MASTER_ON_TRAVIS = os.environ.get("TRAVIS_BRANCH", "master") != "master" \ No newline at end of file +RUNNING_ON_CI = os.environ.get("APPVEYOR", False) +RUNNING_TEST_FOR_MASTER_ON_CI = os.environ.get("APPVEYOR_REPO_BRANCH", "master") != "master" diff --git a/tests/unit/cli/test_context.py b/tests/unit/cli/test_context.py index d834d187d2..2ef94cff61 100644 --- a/tests/unit/cli/test_context.py +++ b/tests/unit/cli/test_context.py @@ -19,7 +19,8 @@ def test_must_set_get_debug_flag(self): ctx.debug = True self.assertEquals(ctx.debug, True, "debug must be set to True") - self.assertEquals(logging.getLogger().getEffectiveLevel(), logging.DEBUG) + self.assertEquals(logging.getLogger('samcli').getEffectiveLevel(), logging.DEBUG) + self.assertEquals(logging.getLogger('aws_lambda_builders').getEffectiveLevel(), logging.DEBUG) def test_must_unset_get_debug_flag(self): ctx = Context() diff --git a/tests/unit/cli/test_global_config.py b/tests/unit/cli/test_global_config.py index 48a0193221..d59520e14b 100644 --- a/tests/unit/cli/test_global_config.py +++ b/tests/unit/cli/test_global_config.py @@ -19,6 +19,16 @@ def test_config_write_error(self): installation_id = gc.installation_id self.assertIsNone(installation_id) + def test_unable_to_create_dir(self): + m = mock_open() + m.side_effect = OSError("Permission DENIED") + gc = GlobalConfig() + with patch('samcli.cli.global_config.Path.mkdir', m): + installation_id = gc.installation_id + self.assertIsNone(installation_id) + telemetry_enabled = gc.telemetry_enabled + self.assertFalse(telemetry_enabled) + def test_setter_cannot_open_path(self): m = mock_open() m.side_effect = IOError("fail") diff --git a/tests/unit/commands/local/lib/swagger/test_parser.py b/tests/unit/commands/local/lib/swagger/test_parser.py index 59db1ea969..827f49be1c 100644 --- a/tests/unit/commands/local/lib/swagger/test_parser.py +++ b/tests/unit/commands/local/lib/swagger/test_parser.py @@ -1,14 +1,14 @@ """ Test the swagger parser """ - -from samcli.commands.local.lib.swagger.parser import SwaggerParser -from samcli.commands.local.lib.provider import Api - from unittest import TestCase + from mock import patch, Mock from parameterized import parameterized, param +from samcli.commands.local.lib.swagger.parser import SwaggerParser +from samcli.local.apigw.local_apigw_service import Route + class TestSwaggerParser_get_apis(TestCase): @@ -31,8 +31,8 @@ def test_with_one_path_method(self): parser._get_integration_function_name = Mock() parser._get_integration_function_name.return_value = function_name - expected = [Api(path="/path1", method="get", function_name=function_name, cors=None)] - result = parser.get_apis() + expected = [Route(path="/path1", methods=["get"], function_name=function_name)] + result = parser.get_routes() self.assertEquals(expected, result) parser._get_integration_function_name.assert_called_with({ @@ -77,11 +77,11 @@ def test_with_combination_of_paths_methods(self): parser._get_integration_function_name.return_value = function_name expected = { - Api(path="/path1", method="get", function_name=function_name, cors=None), - Api(path="/path1", method="delete", function_name=function_name, cors=None), - Api(path="/path2", method="post", function_name=function_name, cors=None), + Route(path="/path1", methods=["get"], function_name=function_name), + Route(path="/path1", methods=["delete"], function_name=function_name), + Route(path="/path2", methods=["post"], function_name=function_name), } - result = parser.get_apis() + result = parser.get_routes() self.assertEquals(expected, set(result)) @@ -104,8 +104,9 @@ def test_with_any_method(self): parser._get_integration_function_name = Mock() parser._get_integration_function_name.return_value = function_name - expected = [Api(path="/path1", method="ANY", function_name=function_name, cors=None)] - result = parser.get_apis() + expected = [Route(methods=["ANY"], path="/path1", + function_name=function_name)] + result = parser.get_routes() self.assertEquals(expected, result) @@ -128,7 +129,7 @@ def test_does_not_have_function_name(self): parser._get_integration_function_name.return_value = None # Function Name could not be resolved expected = [] - result = parser.get_apis() + result = parser.get_routes() self.assertEquals(expected, result) @@ -146,9 +147,8 @@ def test_does_not_have_function_name(self): }}) ]) def test_invalid_swagger(self, test_case_name, swagger): - parser = SwaggerParser(swagger) - result = parser.get_apis() + result = parser.get_routes() expected = [] self.assertEquals(expected, result) diff --git a/tests/unit/commands/local/lib/swagger/test_reader.py b/tests/unit/commands/local/lib/swagger/test_reader.py index 8112b2f21c..f329e464e4 100644 --- a/tests/unit/commands/local/lib/swagger/test_reader.py +++ b/tests/unit/commands/local/lib/swagger/test_reader.py @@ -8,7 +8,7 @@ from parameterized import parameterized, param from mock import Mock, patch -from samcli.commands.local.lib.swagger.reader import parse_aws_include_transform, SamSwaggerReader +from samcli.commands.local.lib.swagger.reader import parse_aws_include_transform, SwaggerReader class TestParseAwsIncludeTransform(TestCase): @@ -57,7 +57,7 @@ class TestSamSwaggerReader_init(TestCase): def test_definition_body_and_uri_required(self): with self.assertRaises(ValueError): - SamSwaggerReader() + SwaggerReader() class TestSamSwaggerReader_read(TestCase): @@ -67,7 +67,7 @@ def test_must_read_first_from_definition_body(self): uri = "./file.txt" expected = {"some": "value"} - reader = SamSwaggerReader(definition_body=body, definition_uri=uri) + reader = SwaggerReader(definition_body=body, definition_uri=uri) reader._download_swagger = Mock() reader._read_from_definition_body = Mock() reader._read_from_definition_body.return_value = expected @@ -82,7 +82,7 @@ def test_read_from_definition_uri(self): uri = "./file.txt" expected = {"some": "value"} - reader = SamSwaggerReader(definition_uri=uri) + reader = SwaggerReader(definition_uri=uri) reader._download_swagger = Mock() reader._download_swagger.return_value = expected @@ -96,7 +96,7 @@ def test_must_use_definition_uri_if_body_does_not_exist(self): uri = "./file.txt" expected = {"some": "value"} - reader = SamSwaggerReader(definition_body=body, definition_uri=uri) + reader = SwaggerReader(definition_body=body, definition_uri=uri) reader._download_swagger = Mock() reader._download_swagger.return_value = expected @@ -119,7 +119,7 @@ def test_must_work_with_include_transform(self, parse_mock): expected = {'k': 'v'} location = "some location" - reader = SamSwaggerReader(definition_body=body) + reader = SwaggerReader(definition_body=body) reader._download_swagger = Mock() reader._download_swagger.return_value = expected parse_mock.return_value = location @@ -132,7 +132,7 @@ def test_must_work_with_include_transform(self, parse_mock): def test_must_get_body_directly(self, parse_mock): body = {'this': 'swagger'} - reader = SamSwaggerReader(definition_body=body) + reader = SwaggerReader(definition_body=body) parse_mock.return_value = None # No location is returned from aws_include parser actual = reader._read_from_definition_body() @@ -151,7 +151,7 @@ def test_must_download_from_s3_for_s3_locations(self, yaml_parse_mock): swagger_str = "some swagger str" expected = "some data" - reader = SamSwaggerReader(definition_uri=location) + reader = SwaggerReader(definition_uri=location) reader._download_from_s3 = Mock() reader._download_from_s3.return_value = swagger_str yaml_parse_mock.return_value = expected @@ -169,7 +169,7 @@ def test_must_skip_non_s3_dictionaries(self, yaml_parse_mock): location = {"some": "value"} - reader = SamSwaggerReader(definition_uri=location) + reader = SwaggerReader(definition_uri=location) reader._download_from_s3 = Mock() actual = reader._download_swagger(location) @@ -184,7 +184,7 @@ def test_must_read_from_local_file(self, yaml_parse_mock): expected = "parsed result" yaml_parse_mock.return_value = expected - with tempfile.NamedTemporaryFile(mode='w') as fp: + with tempfile.NamedTemporaryFile(mode='w', delete=False) as fp: filepath = fp.name json.dump(data, fp) @@ -193,7 +193,7 @@ def test_must_read_from_local_file(self, yaml_parse_mock): cwd = os.path.dirname(filepath) filename = os.path.basename(filepath) - reader = SamSwaggerReader(definition_uri=filename, working_dir=cwd) + reader = SwaggerReader(definition_uri=filename, working_dir=cwd) actual = reader._download_swagger(filename) self.assertEquals(actual, expected) @@ -205,13 +205,13 @@ def test_must_read_from_local_file_without_working_directory(self, yaml_parse_mo expected = "parsed result" yaml_parse_mock.return_value = expected - with tempfile.NamedTemporaryFile(mode='w') as fp: + with tempfile.NamedTemporaryFile(mode='w', delete=False) as fp: filepath = fp.name json.dump(data, fp) fp.flush() - reader = SamSwaggerReader(definition_uri=filepath) + reader = SwaggerReader(definition_uri=filepath) actual = reader._download_swagger(filepath) self.assertEquals(actual, expected) @@ -222,7 +222,7 @@ def test_must_return_none_if_file_not_found(self, yaml_parse_mock): expected = "parsed result" yaml_parse_mock.return_value = expected - reader = SamSwaggerReader(definition_uri="somepath") + reader = SwaggerReader(definition_uri="somepath") actual = reader._download_swagger("abcdefgh.txt") self.assertIsNone(actual) @@ -230,7 +230,7 @@ def test_must_return_none_if_file_not_found(self, yaml_parse_mock): def test_with_invalid_location(self): - reader = SamSwaggerReader(definition_uri="something") + reader = SwaggerReader(definition_uri="something") actual = reader._download_swagger({}) self.assertIsNone(actual) @@ -256,7 +256,7 @@ def test_must_download_file_from_s3(self, tempfilemock, botomock): expected = "data from file" fp_mock.read.return_value = expected - actual = SamSwaggerReader._download_from_s3(self.bucket, self.key, self.version) + actual = SwaggerReader._download_from_s3(self.bucket, self.key, self.version) self.assertEquals(actual, expected) s3_mock.download_fileobj.assert_called_with(self.bucket, self.key, fp_mock, @@ -277,7 +277,7 @@ def test_must_fail_on_download_from_s3(self, tempfilemock, botomock): "download_file") with self.assertRaises(Exception) as cm: - SamSwaggerReader._download_from_s3(self.bucket, self.key) + SwaggerReader._download_from_s3(self.bucket, self.key) self.assertIn(cm.exception.__class__, (botocore.exceptions.NoCredentialsError, botocore.exceptions.ClientError)) @@ -294,7 +294,7 @@ def test_must_work_without_object_version_id(self, tempfilemock, botomock): expected = "data from file" fp_mock.read.return_value = expected - actual = SamSwaggerReader._download_from_s3(self.bucket, self.key) + actual = SwaggerReader._download_from_s3(self.bucket, self.key) self.assertEquals(actual, expected) s3_mock.download_fileobj.assert_called_with(self.bucket, self.key, fp_mock, @@ -313,7 +313,7 @@ def test_must_log_on_download_exception(self, tempfilemock, botomock): "download_file") with self.assertRaises(botocore.exceptions.ClientError): - SamSwaggerReader._download_from_s3(self.bucket, self.key) + SwaggerReader._download_from_s3(self.bucket, self.key) fp_mock.read.assert_not_called() @@ -332,7 +332,7 @@ def test_must_parse_valid_dict(self): "Version": self.version } - result = SamSwaggerReader._parse_s3_location(location) + result = SwaggerReader._parse_s3_location(location) self.assertEquals(result, (self.bucket, self.key, self.version)) def test_must_parse_dict_without_version(self): @@ -341,19 +341,19 @@ def test_must_parse_dict_without_version(self): "Key": self.key } - result = SamSwaggerReader._parse_s3_location(location) + result = SwaggerReader._parse_s3_location(location) self.assertEquals(result, (self.bucket, self.key, None)) def test_must_parse_s3_uri_string(self): location = "s3://{}/{}?versionId={}".format(self.bucket, self.key, self.version) - result = SamSwaggerReader._parse_s3_location(location) + result = SwaggerReader._parse_s3_location(location) self.assertEquals(result, (self.bucket, self.key, self.version)) def test_must_parse_s3_uri_string_without_version_id(self): location = "s3://{}/{}".format(self.bucket, self.key) - result = SamSwaggerReader._parse_s3_location(location) + result = SwaggerReader._parse_s3_location(location) self.assertEquals(result, (self.bucket, self.key, None)) @parameterized.expand([ @@ -364,5 +364,5 @@ def test_must_parse_s3_uri_string_without_version_id(self): ]) def test_must_parse_invalid_location(self, location): - result = SamSwaggerReader._parse_s3_location(location) + result = SwaggerReader._parse_s3_location(location) self.assertEquals(result, (None, None, None)) diff --git a/tests/unit/commands/local/lib/test_api_provider.py b/tests/unit/commands/local/lib/test_api_provider.py new file mode 100644 index 0000000000..013405429a --- /dev/null +++ b/tests/unit/commands/local/lib/test_api_provider.py @@ -0,0 +1,207 @@ +from collections import OrderedDict +from unittest import TestCase + +from mock import patch + +from samcli.commands.local.lib.provider import Api +from samcli.commands.local.lib.api_provider import ApiProvider +from samcli.commands.local.lib.sam_api_provider import SamApiProvider +from samcli.commands.local.lib.cfn_api_provider import CfnApiProvider + + +class TestApiProvider_init(TestCase): + + @patch.object(ApiProvider, "_extract_api") + @patch("samcli.commands.local.lib.api_provider.SamBaseProvider") + def test_provider_with_valid_template(self, SamBaseProviderMock, extract_api_mock): + extract_api_mock.return_value = Api(routes={"set", "of", "values"}) + template = {"Resources": {"a": "b"}} + SamBaseProviderMock.get_template.return_value = template + + provider = ApiProvider(template) + self.assertEquals(len(provider.routes), 3) + self.assertEquals(provider.routes, set(["set", "of", "values"])) + + self.assertEquals(provider.template_dict, {"Resources": {"a": "b"}}) + self.assertEquals(provider.resources, {"a": "b"}) + + +class TestApiProviderSelection(TestCase): + def test_default_provider(self): + resources = { + "TestApi": { + "Type": "AWS::UNKNOWN_TYPE", + "Properties": { + "StageName": "dev", + "DefinitionBody": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + } + + provider = ApiProvider.find_api_provider(resources) + self.assertTrue(isinstance(provider, SamApiProvider)) + + def test_api_provider_sam_api(self): + resources = { + "TestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "dev", + "DefinitionBody": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + } + + provider = ApiProvider.find_api_provider(resources) + self.assertTrue(isinstance(provider, SamApiProvider)) + + def test_api_provider_sam_function(self): + resources = { + "TestApi": { + "Type": "AWS::Serverless::Function", + "Properties": { + "StageName": "dev", + "DefinitionBody": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + } + + provider = ApiProvider.find_api_provider(resources) + + self.assertTrue(isinstance(provider, SamApiProvider)) + + def test_api_provider_cloud_formation(self): + resources = { + "TestApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "StageName": "dev", + "Body": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + } + + provider = ApiProvider.find_api_provider(resources) + self.assertTrue(isinstance(provider, CfnApiProvider)) + + def test_multiple_api_provider_cloud_formation(self): + resources = OrderedDict() + resources["TestApi"] = { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "StageName": "dev", + "Body": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + resources["OtherApi"] = { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "dev", + "DefinitionBody": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + + provider = ApiProvider.find_api_provider(resources) + self.assertTrue(isinstance(provider, CfnApiProvider)) diff --git a/tests/unit/commands/local/lib/test_cfn_api_provider.py b/tests/unit/commands/local/lib/test_cfn_api_provider.py new file mode 100644 index 0000000000..85388467b3 --- /dev/null +++ b/tests/unit/commands/local/lib/test_cfn_api_provider.py @@ -0,0 +1,920 @@ +import json +import tempfile +from collections import OrderedDict +from unittest import TestCase + +from mock import patch +from six import assertCountEqual + +from samcli.commands.local.lib.api_provider import ApiProvider +from samcli.commands.local.lib.cfn_api_provider import CfnApiProvider +from samcli.local.apigw.local_apigw_service import Route +from tests.unit.commands.local.lib.test_sam_api_provider import make_swagger + + +class TestApiProviderWithApiGatewayRestRoute(TestCase): + + def setUp(self): + self.binary_types = ["image/png", "image/jpg"] + self.input_routes = [ + Route(path="/path1", methods=["GET", "POST"], function_name="SamFunc1"), + Route(path="/path2", methods=["PUT", "GET"], function_name="SamFunc1"), + Route(path="/path3", methods=["DELETE"], function_name="SamFunc1") + ] + + def test_with_no_apis(self): + template = { + "Resources": { + + "Api1": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + }, + + } + } + } + + provider = ApiProvider(template) + + self.assertEquals(provider.routes, []) + + def test_with_inline_swagger_apis(self): + template = { + "Resources": { + + "Api1": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": make_swagger(self.input_routes) + } + } + } + } + + provider = ApiProvider(template) + assertCountEqual(self, self.input_routes, provider.routes) + + def test_with_swagger_as_local_file(self): + with tempfile.NamedTemporaryFile(mode='w', delete=False) as fp: + filename = fp.name + + swagger = make_swagger(self.input_routes) + + json.dump(swagger, fp) + fp.flush() + + template = { + "Resources": { + + "Api1": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BodyS3Location": filename + } + } + } + } + + provider = ApiProvider(template) + assertCountEqual(self, self.input_routes, provider.routes) + + def test_body_with_swagger_as_local_file_expect_fail(self): + with tempfile.NamedTemporaryFile(mode='w', delete=False) as fp: + filename = fp.name + + swagger = make_swagger(self.input_routes) + + json.dump(swagger, fp) + fp.flush() + + template = { + "Resources": { + + "Api1": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": filename + } + } + } + } + self.assertRaises(Exception, ApiProvider, template) + + @patch("samcli.commands.local.lib.cfn_base_api_provider.SwaggerReader") + def test_with_swagger_as_both_body_and_uri_called(self, SwaggerReaderMock): + body = {"some": "body"} + filename = "somefile.txt" + + template = { + "Resources": { + + "Api1": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BodyS3Location": filename, + "Body": body + } + } + } + } + + SwaggerReaderMock.return_value.read.return_value = make_swagger(self.input_routes) + + cwd = "foo" + provider = ApiProvider(template, cwd=cwd) + assertCountEqual(self, self.input_routes, provider.routes) + SwaggerReaderMock.assert_called_with(definition_body=body, definition_uri=filename, working_dir=cwd) + + def test_swagger_with_any_method(self): + routes = [ + Route(path="/path", methods=["any"], function_name="SamFunc1") + ] + + expected_routes = [ + Route(path="/path", methods=["GET", + "DELETE", + "PUT", + "POST", + "HEAD", + "OPTIONS", + "PATCH"], function_name="SamFunc1") + ] + + template = { + "Resources": { + "Api1": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": make_swagger(routes) + } + } + } + } + + provider = ApiProvider(template) + assertCountEqual(self, expected_routes, provider.routes) + + def test_with_binary_media_types(self): + template = { + "Resources": { + + "Api1": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": make_swagger(self.input_routes, binary_media_types=self.binary_types) + } + } + } + } + + expected_binary_types = sorted(self.binary_types) + expected_apis = [ + Route(path="/path1", methods=["GET", "POST"], function_name="SamFunc1"), + Route(path="/path2", methods=["PUT", "GET"], function_name="SamFunc1"), + Route(path="/path3", methods=["DELETE"], function_name="SamFunc1") + ] + + provider = ApiProvider(template) + assertCountEqual(self, expected_apis, provider.routes) + assertCountEqual(self, provider.api.binary_media_types, expected_binary_types) + + def test_with_binary_media_types_in_swagger_and_on_resource(self): + input_routes = [ + Route(path="/path", methods=["OPTIONS"], function_name="SamFunc1"), + ] + extra_binary_types = ["text/html"] + + template = { + "Resources": { + + "Api1": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BinaryMediaTypes": extra_binary_types, + "Body": make_swagger(input_routes, binary_media_types=self.binary_types) + } + } + } + } + + expected_binary_types = sorted(self.binary_types + extra_binary_types) + expected_routes = [ + Route(path="/path", methods=["OPTIONS"], function_name="SamFunc1"), + ] + + provider = ApiProvider(template) + assertCountEqual(self, expected_routes, provider.routes) + assertCountEqual(self, provider.api.binary_media_types, expected_binary_types) + + +class TestCloudFormationStageValues(TestCase): + def setUp(self): + self.binary_types = ["image/png", "image/jpg"] + self.input_routes = [ + Route(path="/path1", methods=["GET", "POST"], function_name="SamFunc1"), + Route(path="/path2", methods=["PUT", "GET"], function_name="SamFunc1"), + Route(path="/path3", methods=["DELETE"], function_name="SamFunc1") + ] + + def test_provider_parse_stage_name(self): + template = { + "Resources": { + "Stage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "StageName": "dev", + "RestApiId": "TestApi" + } + }, + "TestApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + } + } + provider = ApiProvider(template) + route1 = Route(path='/path', methods=['GET'], function_name='NoApiEventFunction') + + self.assertIn(route1, provider.routes) + self.assertEquals(provider.api.stage_name, "dev") + self.assertEquals(provider.api.stage_variables, None) + + def test_provider_stage_variables(self): + template = { + "Resources": { + "Stage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "StageName": "dev", + "Variables": { + "vis": "data", + "random": "test", + "foo": "bar" + }, + "RestApiId": "TestApi" + } + }, + "TestApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + } + } + provider = ApiProvider(template) + route1 = Route(path='/path', methods=['GET'], function_name='NoApiEventFunction') + self.assertIn(route1, provider.routes) + self.assertEquals(provider.api.stage_name, "dev") + self.assertEquals(provider.api.stage_variables, { + "vis": "data", + "random": "test", + "foo": "bar" + }) + + def test_multi_stage_get_all(self): + resources = OrderedDict({ + "ProductionApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + }, + "/anotherpath": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations", + }, + "responses": {}, + }, + } + } + + } + } + } + } + }) + resources["StageDev"] = { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "StageName": "dev", + "Variables": { + "vis": "data", + "random": "test", + "foo": "bar" + }, + "RestApiId": "ProductionApi" + } + } + resources["StageProd"] = { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "StageName": "Production", + "Variables": { + "vis": "prod data", + "random": "test", + "foo": "bar" + }, + "RestApiId": "ProductionApi" + }, + } + template = {"Resources": resources} + provider = ApiProvider(template) + + result = [f for f in provider.get_all()] + routes = result[0].routes + + route1 = Route(path='/path', methods=['GET'], function_name='NoApiEventFunction') + route2 = Route(path='/anotherpath', methods=['POST'], function_name='NoApiEventFunction') + self.assertEquals(len(routes), 2) + self.assertIn(route1, routes) + self.assertIn(route2, routes) + + self.assertEquals(provider.api.stage_name, "Production") + self.assertEquals(provider.api.stage_variables, { + "vis": "prod data", + "random": "test", + "foo": "bar" + }) + + +class TestCloudFormationResourceMethod(TestCase): + + def setUp(self): + self.binary_types = ["image/png", "image/jpg"] + self.input_routes = [ + Route(path="/path1", methods=["GET", "POST"], function_name="SamFunc1"), + Route(path="/path2", methods=["PUT", "GET"], function_name="SamFunc1"), + Route(path="/path3", methods=["DELETE"], function_name="SamFunc1") + ] + + def test_basic_rest_api_resource_method(self): + template = { + "Resources": { + "TestApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "StageName": "Prod" + } + }, + "ApiResource": { + "Properties": { + "PathPart": "{proxy+}", + "RestApiId": "TestApi", + } + }, + "ApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "RestApiId": "TestApi", + "ResourceId": "ApiResource" + }, + } + } + } + + provider = ApiProvider(template) + + self.assertEquals(provider.routes, [Route(function_name=None, path="/{proxy+}", methods=["POST"])]) + + def test_resolve_correct_resource_path(self): + resources = { + "RootApiResource": { + "Tyoe": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "root", + "ResourceId": "TestApi", + } + } + } + beta_resource = { + "Tyoe": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "beta", + "ResourceId": "TestApi", + "ParentId": "RootApiResource" + } + } + resources["BetaApiResource"] = beta_resource + provider = CfnApiProvider() + full_path = provider.resolve_resource_path(resources, beta_resource, "/test") + self.assertEquals(full_path, "/root/beta/test") + + def test_resolve_correct_multi_parent_resource_path(self): + template = { + "Resources": { + "TestApi": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "StageName": "Prod" + } + }, + "RootApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "root", + "ResourceId": "TestApi", + } + }, + "V1ApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "v1", + "ResourceId": "TestApi", + "ParentId": "RootApiResource" + } + }, + "AlphaApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "alpha", + "ResourceId": "TestApi", + "ParentId": "V1ApiResource" + } + }, + "BetaApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "beta", + "ResourceId": "TestApi", + "ParentId": "V1ApiResource" + } + }, + "AlphaApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "RestApiId": "TestApi", + "ResourceId": "AlphaApiResource" + }, + }, + "BetaAlphaApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "RestApiId": "TestApi", + "ResourceId": "BetaApiResource" + }, + } + } + } + + provider = ApiProvider(template) + assertCountEqual(self, provider.routes, [Route(path="/root/v1/beta", methods=["POST"], function_name=None), + Route(path="/root/v1/alpha", methods=["GET"], function_name=None)]) + + def test_resource_with_method_correct_routes(self): + template = { + "Resources": { + "TestApi": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "StageName": "Prod" + } + }, + "BetaApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "beta", + "ResourceId": "TestApi", + } + }, + "BetaAlphaApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "RestApiId": "TestApi", + "ResourceId": "BetaApiResource", + }, + } + } + } + provider = ApiProvider(template) + assertCountEqual(self, provider.routes, + [Route(path="/beta", methods=["POST", "GET", "DELETE", "HEAD", "OPTIONS", "PATCH", "PUT"], + function_name=None), + ]) + + def test_method_integration_uri(self): + template = { + "Resources": { + "TestApi": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "StageName": "Prod" + } + }, + "RootApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "root", + "ResourceId": "TestApi", + } + }, + "V1ApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "v1", + "ResourceId": "TestApi", + "ParentId": "RootApiResource" + } + }, + "AlphaApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "alpha", + "ResourceId": "TestApi", + "ParentId": "V1ApiResource" + } + }, + "BetaApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "beta", + "ResourceId": "TestApi", + "ParentId": "V1ApiResource" + } + }, + "AlphaApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "RestApiId": "TestApi", + "ResourceId": "AlphaApiResource", + "Integration": { + "Uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/" + "functions" + "/${AWSBetaLambdaFunction.Arn}/invocations} " + } + } + }, + }, + "BetaAlphaApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "RestApiId": "TestApi", + "ResourceId": "BetaApiResource", + "Integration": { + "Uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/" + "functions" + "/${AWSLambdaFunction.Arn}/invocations}" + } + } + }, + }, + "AWSAlphaLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": ".", + "Handler": "main.run_test", + "Runtime": "Python3.6" + } + }, + "AWSBetaLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": ".", + "Handler": "main.run_test", + "Runtime": "Python3.6" + } + } + } + } + + provider = ApiProvider(template) + assertCountEqual(self, provider.routes, + [Route(path="/root/v1/beta", methods=["POST"], function_name="AWSLambdaFunction"), + Route(path="/root/v1/alpha", methods=["GET"], function_name="AWSBetaLambdaFunction")]) + + def test_binary_media_types_method(self): + template = { + "Resources": { + "TestApi": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "StageName": "Prod" + } + }, + "RootApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "root", + "ResourceId": "TestApi", + } + }, + "V1ApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "v1", + "ResourceId": "TestApi", + "ParentId": "RootApiResource" + } + }, + "AlphaApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "alpha", + "ResourceId": "TestApi", + "ParentId": "V1ApiResource" + } + }, + "BetaApiResource": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "PathPart": "beta", + "ResourceId": "TestApi", + "ParentId": "V1ApiResource" + } + }, + "AlphaApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "GET", + "RestApiId": "TestApi", + "ResourceId": "AlphaApiResource", + "Integration": { + "Uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/" + "functions" + "/${AWSBetaLambdaFunction.Arn}/invocations} " + }, + "ContentHandling": "CONVERT_TO_BINARY", + "ContentType": "image~1jpg" + } + }, + }, + "BetaAlphaApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "POST", + "RestApiId": "TestApi", + "ResourceId": "BetaApiResource", + "Integration": { + "Uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/" + "functions" + "/${AWSLambdaFunction.Arn}/invocations}" + }, + "ContentHandling": "CONVERT_TO_BINARY", + "ContentType": "image~1png" + } + }, + }, + "AWSAlphaLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": ".", + "Handler": "main.run_test", + "Runtime": "Python3.6" + } + }, + "AWSBetaLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": ".", + "Handler": "main.run_test", + "Runtime": "Python3.6" + } + } + } + } + + provider = ApiProvider(template) + assertCountEqual(self, provider.api.binary_media_types, ["image/png", "image/jpg"]) + + def test_cdk(self): + template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": ".", + "Handler": "main.handler", + "Runtime": "python3.6", + }, + "DependsOn": [ + "HelloHandlerServiceRole11EF7C63" + ], + }, + "EndpointEEF1FD8F": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "Endpoint" + } + }, + "EndpointDeploymentStageprodB78BEEA0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "DeploymentId": { + "Ref": "EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf" + }, + "StageName": "prod" + } + }, + "Endpointproxy39E2174E": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "EndpointEEF1FD8F", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + } + } + }, + "EndpointproxyANYC09721C5": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "Endpointproxy39E2174E" + }, + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + "lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "EndpointANY485C938B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "EndpointEEF1FD8F", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + "lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + } + }, + "Parameters": { + "HelloHandlerCodeS3Bucket4359A483": { + "Type": "String", + "Description": "S3 bucket for asset \"CdkWorkshopStack/HelloHandler/Code\"" + }, + "HelloHandlerCodeS3VersionKey07D12610": { + "Type": "String", + "Description": "S3 key for asset version \"CdkWorkshopStack/HelloHandler/Code\"" + } + }, + "Outputs": { + "Endpoint8024A810": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "EndpointEEF1FD8F" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "EndpointDeploymentStageprodB78BEEA0" + }, + "/" + ] + ] + }, + "Export": { + "Name": "CdkWorkshopStack:Endpoint8024A810" + } + } + } + } + provider = ApiProvider(template) + proxy_paths = [Route(path="/{proxy+}", methods=Route.ANY_HTTP_METHODS, function_name="HelloHandler2E4FBA4D")] + root_paths = [Route(path="/", methods=Route.ANY_HTTP_METHODS, function_name="HelloHandler2E4FBA4D")] + assertCountEqual(self, provider.routes, proxy_paths + root_paths) diff --git a/tests/unit/commands/local/lib/test_local_api_service.py b/tests/unit/commands/local/lib/test_local_api_service.py index 3cc5d2c4c3..f43f93713e 100644 --- a/tests/unit/commands/local/lib/test_local_api_service.py +++ b/tests/unit/commands/local/lib/test_local_api_service.py @@ -6,10 +6,11 @@ from mock import Mock, patch -from samcli.commands.local.lib import provider +from samcli.commands.local.lib.provider import Api +from samcli.commands.local.lib.api_collector import ApiCollector +from samcli.commands.local.lib.api_provider import ApiProvider from samcli.commands.local.lib.exceptions import NoApisDefined from samcli.commands.local.lib.local_api_service import LocalApiService -from samcli.commands.local.lib.provider import Api from samcli.local.apigw.local_apigw_service import Route @@ -35,12 +36,10 @@ def setUp(self): self.lambda_invoke_context_mock.stderr = self.stderr_mock @patch("samcli.commands.local.lib.local_api_service.LocalApigwService") - @patch("samcli.commands.local.lib.local_api_service.SamApiProvider") + @patch("samcli.commands.local.lib.local_api_service.ApiProvider") @patch.object(LocalApiService, "_make_static_dir_path") @patch.object(LocalApiService, "_print_routes") - @patch.object(LocalApiService, "_make_routing_list") def test_must_start_service(self, - make_routing_list_mock, log_routes_mock, make_static_dir_mock, SamApiProviderMock, @@ -48,7 +47,6 @@ def test_must_start_service(self, routing_list = [1, 2, 3] # something static_dir_path = "/foo/bar" - make_routing_list_mock.return_value = routing_list make_static_dir_mock.return_value = static_dir_path SamApiProviderMock.return_value = self.api_provider_mock @@ -56,6 +54,7 @@ def test_must_start_service(self, # Now start the service local_service = LocalApiService(self.lambda_invoke_context_mock, self.port, self.host, self.static_dir) + local_service.api_provider.api.routes = routing_list local_service.start() # Make sure the right methods are called @@ -63,10 +62,9 @@ def test_must_start_service(self, cwd=self.cwd, parameter_overrides=self.lambda_invoke_context_mock.parameter_overrides) - make_routing_list_mock.assert_called_with(self.api_provider_mock) - log_routes_mock.assert_called_with(self.api_provider_mock, self.host, self.port) + log_routes_mock.assert_called_with(routing_list, self.host, self.port) make_static_dir_mock.assert_called_with(self.cwd, self.static_dir) - ApiGwServiceMock.assert_called_with(routing_list=routing_list, + ApiGwServiceMock.assert_called_with(api=self.api_provider_mock.api, lambda_runner=self.lambda_runner_mock, static_dir=static_dir_path, port=self.port, @@ -77,75 +75,50 @@ def test_must_start_service(self, self.apigw_service.run.assert_called_with() @patch("samcli.commands.local.lib.local_api_service.LocalApigwService") - @patch("samcli.commands.local.lib.local_api_service.SamApiProvider") + @patch("samcli.commands.local.lib.local_api_service.ApiProvider") @patch.object(LocalApiService, "_make_static_dir_path") @patch.object(LocalApiService, "_print_routes") - @patch.object(LocalApiService, "_make_routing_list") + @patch.object(ApiProvider, "_extract_api") def test_must_raise_if_route_not_available(self, - make_routing_list_mock, + extract_api, log_routes_mock, make_static_dir_mock, SamApiProviderMock, ApiGwServiceMock): routing_list = [] # Empty - - make_routing_list_mock.return_value = routing_list - + api = Api() + extract_api.return_value = api + SamApiProviderMock.extract_api.return_value = api SamApiProviderMock.return_value = self.api_provider_mock ApiGwServiceMock.return_value = self.apigw_service # Now start the service local_service = LocalApiService(self.lambda_invoke_context_mock, self.port, self.host, self.static_dir) - + local_service.api_provider.api.routes = routing_list with self.assertRaises(NoApisDefined): local_service.start() -class TestLocalApiService_make_routing_list(TestCase): - - def test_must_return_routing_list_from_apis(self): - api_provider = Mock() - apis = [ - Api(path="/1", method="GET1", function_name="name1", cors="CORS1"), - Api(path="/2", method="GET2", function_name="name2", cors="CORS2"), - Api(path="/3", method="GET3", function_name="name3", cors="CORS3"), - ] - expected = [ - Route(path="/1", methods=["GET1"], function_name="name1"), - Route(path="/2", methods=["GET2"], function_name="name2"), - Route(path="/3", methods=["GET3"], function_name="name3") - ] - - api_provider.get_all.return_value = apis - - result = LocalApiService._make_routing_list(api_provider) - self.assertEquals(len(result), len(expected)) - for index, r in enumerate(result): - self.assertEquals(r.__dict__, expected[index].__dict__) - - class TestLocalApiService_print_routes(TestCase): def test_must_print_routes(self): host = "host" port = 123 - api_provider = Mock() apis = [ - Api(path="/1", method="GET", function_name="name1", cors="CORS1"), - Api(path="/1", method="POST", function_name="name1", cors="CORS1"), - Api(path="/1", method="DELETE", function_name="othername1", cors="CORS1"), - Api(path="/2", method="GET2", function_name="name2", cors="CORS2"), - Api(path="/3", method="GET3", function_name="name3", cors="CORS3"), + Route(path="/1", methods=["GET"], function_name="name1"), + Route(path="/1", methods=["POST"], function_name="name1"), + Route(path="/1", methods=["DELETE"], function_name="othername1"), + Route(path="/2", methods=["GET2"], function_name="name2"), + Route(path="/3", methods=["GET3"], function_name="name3"), ] - api_provider.get_all.return_value = apis - + apis = ApiCollector.dedupe_function_routes(apis) expected = {"Mounting name1 at http://host:123/1 [GET, POST]", "Mounting othername1 at http://host:123/1 [DELETE]", "Mounting name2 at http://host:123/2 [GET2]", "Mounting name3 at http://host:123/3 [GET3]"} - actual = LocalApiService._print_routes(api_provider, host, port) + actual = LocalApiService._print_routes(apis, host, port) self.assertEquals(expected, set(actual)) @@ -181,39 +154,3 @@ def test_must_return_none_if_path_not_exists(self, os_mock): result = LocalApiService._make_static_dir_path(cwd, static_dir) self.assertIsNone(result) - - -class TestRoutingList(TestCase): - - def setUp(self): - self.function_name = "routingTest" - apis = [ - provider.Api(path="/get", method="GET", function_name=self.function_name, cors="cors"), - provider.Api(path="/get", method="GET", function_name=self.function_name, cors="cors", stage_name="Dev"), - provider.Api(path="/post", method="POST", function_name=self.function_name, cors="cors", stage_name="Prod"), - provider.Api(path="/get", method="GET", function_name=self.function_name, cors="cors", - stage_variables={"test": "data"}), - provider.Api(path="/post", method="POST", function_name=self.function_name, cors="cors", stage_name="Prod", - stage_variables={"data": "more data"}), - ] - self.api_provider_mock = Mock() - self.api_provider_mock.get_all.return_value = apis - - def test_make_routing_list(self): - routing_list = LocalApiService._make_routing_list(self.api_provider_mock) - - expected_routes = [ - Route(function_name=self.function_name, methods=['GET'], path='/get', stage_name=None, - stage_variables=None), - Route(function_name=self.function_name, methods=['GET'], path='/get', stage_name='Dev', - stage_variables=None), - Route(function_name=self.function_name, methods=['POST'], path='/post', stage_name='Prod', - stage_variables=None), - Route(function_name=self.function_name, methods=['GET'], path='/get', stage_name=None, - stage_variables={'test': 'data'}), - Route(function_name=self.function_name, methods=['POST'], path='/post', stage_name='Prod', - stage_variables={'data': 'more data'}), - ] - self.assertEquals(len(routing_list), len(expected_routes)) - for index, r in enumerate(routing_list): - self.assertEquals(r.__dict__, expected_routes[index].__dict__) diff --git a/tests/unit/commands/local/lib/test_sam_api_provider.py b/tests/unit/commands/local/lib/test_sam_api_provider.py index 3ac01956be..6215196fa8 100644 --- a/tests/unit/commands/local/lib/test_sam_api_provider.py +++ b/tests/unit/commands/local/lib/test_sam_api_provider.py @@ -1,57 +1,30 @@ -import tempfile import json - +import tempfile +from collections import OrderedDict from unittest import TestCase + from mock import patch from nose_parameterized import parameterized - from six import assertCountEqual -from samcli.commands.local.lib.sam_api_provider import SamApiProvider -from samcli.commands.local.lib.provider import Api from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException - - -class TestSamApiProvider_init(TestCase): - - @patch.object(SamApiProvider, "_extract_apis") - @patch("samcli.commands.local.lib.sam_api_provider.SamBaseProvider") - def test_provider_with_valid_template(self, SamBaseProviderMock, extract_api_mock): - extract_api_mock.return_value = {"set", "of", "values"} - - template = {"Resources": {"a": "b"}} - SamBaseProviderMock.get_template.return_value = template - - provider = SamApiProvider(template) - - self.assertEquals(len(provider.apis), 3) - self.assertEquals(provider.apis, set(["set", "of", "values"])) - self.assertEquals(provider.template_dict, {"Resources": {"a": "b"}}) - self.assertEquals(provider.resources, {"a": "b"}) +from samcli.commands.local.lib.api_provider import ApiProvider +from samcli.commands.local.lib.provider import Cors +from samcli.local.apigw.local_apigw_service import Route class TestSamApiProviderWithImplicitApis(TestCase): - def test_provider_with_no_resource_properties(self): - template = { - "Resources": { - - "SamFunc1": { - "Type": "AWS::Lambda::Function" - } - } - } + template = {"Resources": {"SamFunc1": {"Type": "AWS::Lambda::Function"}}} - provider = SamApiProvider(template) + provider = ApiProvider(template) - self.assertEquals(len(provider.apis), 0) - self.assertEquals(provider.apis, []) + self.assertEquals(provider.routes, []) @parameterized.expand([("GET"), ("get")]) def test_provider_has_correct_api(self, method): template = { "Resources": { - "SamFunc1": { "Type": "AWS::Serverless::Function", "Properties": { @@ -61,27 +34,25 @@ def test_provider_has_correct_api(self, method): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": method - } + "Properties": {"Path": "/path", "Method": method}, } - } - } + }, + }, } } } - provider = SamApiProvider(template) + provider = ApiProvider(template) - self.assertEquals(len(provider.apis), 1) - self.assertEquals(list(provider.apis)[0], Api(path="/path", method="GET", function_name="SamFunc1", cors=None, - stage_name="Prod")) + self.assertEquals(len(provider.routes), 1) + self.assertEquals( + list(provider.routes)[0], + Route(path="/path", methods=["GET"], function_name="SamFunc1"), + ) def test_provider_creates_api_for_all_events(self): template = { "Resources": { - "SamFunc1": { "Type": "AWS::Serverless::Function", "Properties": { @@ -91,37 +62,28 @@ def test_provider_creates_api_for_all_events(self): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": "GET" - } + "Properties": {"Path": "/path", "Method": "GET"}, }, "Event2": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": "POST" - } - } - } - } + "Properties": {"Path": "/path", "Method": "POST"}, + }, + }, + }, } } } - provider = SamApiProvider(template) + provider = ApiProvider(template) - api_event1 = Api(path="/path", method="GET", function_name="SamFunc1", cors=None, stage_name="Prod") - api_event2 = Api(path="/path", method="POST", function_name="SamFunc1", cors=None, stage_name="Prod") + api = Route(path="/path", methods=["GET", "POST"], function_name="SamFunc1") - self.assertIn(api_event1, provider.apis) - self.assertIn(api_event2, provider.apis) - self.assertEquals(len(provider.apis), 2) + self.assertIn(api, provider.routes) + self.assertEquals(len(provider.routes), 1) def test_provider_has_correct_template(self): template = { "Resources": { - "SamFunc1": { "Type": "AWS::Serverless::Function", "Properties": { @@ -131,13 +93,10 @@ def test_provider_has_correct_template(self): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": "GET" - } + "Properties": {"Path": "/path", "Method": "GET"}, } - } - } + }, + }, }, "SamFunc2": { "Type": "AWS::Serverless::Function", @@ -148,29 +107,25 @@ def test_provider_has_correct_template(self): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": "POST" - } + "Properties": {"Path": "/path", "Method": "POST"}, } - } - } - } + }, + }, + }, } } - provider = SamApiProvider(template) + provider = ApiProvider(template) - api1 = Api(path="/path", method="GET", function_name="SamFunc1", cors=None, stage_name="Prod") - api2 = Api(path="/path", method="POST", function_name="SamFunc2", cors=None, stage_name="Prod") + api1 = Route(path="/path", methods=["GET"], function_name="SamFunc1") + api2 = Route(path="/path", methods=["POST"], function_name="SamFunc2") - self.assertIn(api1, provider.apis) - self.assertIn(api2, provider.apis) + self.assertIn(api1, provider.routes) + self.assertIn(api2, provider.routes) def test_provider_with_no_api_events(self): template = { "Resources": { - "SamFunc1": { "Type": "AWS::Serverless::Function", "Properties": { @@ -180,43 +135,39 @@ def test_provider_with_no_api_events(self): "Events": { "Event1": { "Type": "S3", - "Properties": { - "Property1": "value" - } + "Properties": {"Property1": "value"}, } - } - } + }, + }, } } } - provider = SamApiProvider(template) + provider = ApiProvider(template) - self.assertEquals(provider.apis, []) + self.assertEquals(provider.routes, []) def test_provider_with_no_serverless_function(self): template = { "Resources": { - "SamFunc1": { "Type": "AWS::Lambda::Function", "Properties": { "CodeUri": "/usr/foo/bar", "Runtime": "nodejs4.3", - "Handler": "index.handler" - } + "Handler": "index.handler", + }, } } } - provider = SamApiProvider(template) + provider = ApiProvider(template) - self.assertEquals(provider.apis, []) + self.assertEquals(provider.routes, []) def test_provider_get_all(self): template = { "Resources": { - "SamFunc1": { "Type": "AWS::Serverless::Function", "Properties": { @@ -226,13 +177,10 @@ def test_provider_get_all(self): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": "GET" - } + "Properties": {"Path": "/path", "Method": "GET"}, } - } - } + }, + }, }, "SamFunc2": { "Type": "AWS::Serverless::Function", @@ -243,41 +191,38 @@ def test_provider_get_all(self): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": "POST" - } + "Properties": {"Path": "/path", "Method": "POST"}, } - } - } - } + }, + }, + }, } } - provider = SamApiProvider(template) + provider = ApiProvider(template) result = [f for f in provider.get_all()] + routes = result[0].routes + route1 = Route(path="/path", methods=["GET"], function_name="SamFunc1") + route2 = Route(path="/path", methods=["POST"], function_name="SamFunc2") - api1 = Api(path="/path", method="GET", function_name="SamFunc1", stage_name="Prod") - api2 = Api(path="/path", method="POST", function_name="SamFunc2", stage_name="Prod") + self.assertIn(route1, routes) + self.assertIn(route2, routes) - self.assertIn(api1, result) - self.assertIn(api2, result) - - def test_provider_get_all_with_no_apis(self): + def test_provider_get_all_with_no_routes(self): template = {} - provider = SamApiProvider(template) + provider = ApiProvider(template) result = [f for f in provider.get_all()] + routes = result[0].routes - self.assertEquals(result, []) + self.assertEquals(routes, []) @parameterized.expand([("ANY"), ("any")]) def test_provider_with_any_method(self, method): template = { "Resources": { - "SamFunc1": { "Type": "AWS::Serverless::Function", "Properties": { @@ -287,35 +232,24 @@ def test_provider_with_any_method(self, method): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": method - } + "Properties": {"Path": "/path", "Method": method}, } - } - } + }, + }, } } } - provider = SamApiProvider(template) - - api_get = Api(path="/path", method="GET", function_name="SamFunc1", cors=None, stage_name="Prod") - api_post = Api(path="/path", method="POST", function_name="SamFunc1", cors=None, stage_name="Prod") - api_put = Api(path="/path", method="PUT", function_name="SamFunc1", cors=None, stage_name="Prod") - api_delete = Api(path="/path", method="DELETE", function_name="SamFunc1", cors=None, stage_name="Prod") - api_patch = Api(path="/path", method="PATCH", function_name="SamFunc1", cors=None, stage_name="Prod") - api_head = Api(path="/path", method="HEAD", function_name="SamFunc1", cors=None, stage_name="Prod") - api_options = Api(path="/path", method="OPTIONS", function_name="SamFunc1", cors=None, stage_name="Prod") - - self.assertEquals(len(provider.apis), 7) - self.assertIn(api_get, provider.apis) - self.assertIn(api_post, provider.apis) - self.assertIn(api_put, provider.apis) - self.assertIn(api_delete, provider.apis) - self.assertIn(api_patch, provider.apis) - self.assertIn(api_head, provider.apis) - self.assertIn(api_options, provider.apis) + provider = ApiProvider(template) + + api1 = Route( + path="/path", + methods=["GET", "DELETE", "PUT", "POST", "HEAD", "OPTIONS", "PATCH"], + function_name="SamFunc1", + ) + + self.assertEquals(len(provider.routes), 1) + self.assertIn(api1, provider.routes) def test_provider_must_support_binary_media_types(self): template = { @@ -325,12 +259,10 @@ def test_provider_must_support_binary_media_types(self): "image~1gif", "image~1png", "image~1png", # Duplicates must be ignored - {"Ref": "SomeParameter"} # Refs are ignored as well ] } }, "Resources": { - "SamFunc1": { "Type": "AWS::Serverless::Function", "Properties": { @@ -340,37 +272,33 @@ def test_provider_must_support_binary_media_types(self): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": "get" - } + "Properties": {"Path": "/path", "Method": "get"}, } - } - } + }, + }, } - } + }, } - provider = SamApiProvider(template) + provider = ApiProvider(template) - self.assertEquals(len(provider.apis), 1) - self.assertEquals(list(provider.apis)[0], Api(path="/path", method="GET", function_name="SamFunc1", - binary_media_types=["image/gif", "image/png"], cors=None, - stage_name="Prod")) + self.assertEquals(len(provider.routes), 1) + self.assertEquals( + list(provider.routes)[0], + Route(path="/path", methods=["GET"], function_name="SamFunc1"), + ) + + assertCountEqual( + self, provider.api.binary_media_types, ["image/gif", "image/png"] + ) + self.assertEquals(provider.api.stage_name, "Prod") def test_provider_must_support_binary_media_types_with_any_method(self): template = { "Globals": { - "Api": { - "BinaryMediaTypes": [ - "image~1gif", - "image~1png", - "text/html" - ] - } + "Api": {"BinaryMediaTypes": ["image~1gif", "image~1png", "text/html"]} }, "Resources": { - "SamFunc1": { "Type": "AWS::Serverless::Function", "Properties": { @@ -380,159 +308,128 @@ def test_provider_must_support_binary_media_types_with_any_method(self): "Events": { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path", - "Method": "any" - } + "Properties": {"Path": "/path", "Method": "any"}, } - } - } + }, + }, } - } + }, } binary = ["image/gif", "image/png", "text/html"] - expected_apis = [ - Api(path="/path", method="GET", function_name="SamFunc1", binary_media_types=binary, stage_name="Prod"), - Api(path="/path", method="POST", function_name="SamFunc1", binary_media_types=binary, stage_name="Prod"), - Api(path="/path", method="PUT", function_name="SamFunc1", binary_media_types=binary, stage_name="Prod"), - Api(path="/path", method="DELETE", function_name="SamFunc1", binary_media_types=binary, stage_name="Prod"), - Api(path="/path", method="HEAD", function_name="SamFunc1", binary_media_types=binary, stage_name="Prod"), - Api(path="/path", method="OPTIONS", function_name="SamFunc1", binary_media_types=binary, stage_name="Prod"), - Api(path="/path", method="PATCH", function_name="SamFunc1", binary_media_types=binary, stage_name="Prod") + expected_routes = [ + Route( + path="/path", + methods=["GET", "DELETE", "PUT", "POST", "HEAD", "OPTIONS", "PATCH"], + function_name="SamFunc1", + ) ] - provider = SamApiProvider(template) - - assertCountEqual(self, provider.apis, expected_apis) + provider = ApiProvider(template) - def test_convert_event_api_with_invalid_event_properties(self): - properties = { - "Path": "/foo", - "Method": "get", - "RestApiId": { - # This is not supported. Only Ref is supported - "Fn::Sub": "foo" - } - } - - with self.assertRaises(InvalidSamDocumentException): - SamApiProvider._convert_event_api("logicalId", properties) + assertCountEqual(self, provider.routes, expected_routes) + assertCountEqual(self, provider.api.binary_media_types, binary) class TestSamApiProviderWithExplicitApis(TestCase): - def setUp(self): self.binary_types = ["image/png", "image/jpg"] - self.input_apis = [ - Api(path="/path1", method="GET", function_name="SamFunc1", cors=None, stage_name="Prod"), - Api(path="/path1", method="POST", function_name="SamFunc1", cors=None, stage_name="Prod"), - - Api(path="/path2", method="PUT", function_name="SamFunc1", cors=None, stage_name="Prod"), - Api(path="/path2", method="GET", function_name="SamFunc1", cors=None, stage_name="Prod"), - - Api(path="/path3", method="DELETE", function_name="SamFunc1", cors=None, stage_name="Prod") + self.stage_name = "Prod" + self.input_routes = [ + Route(path="/path1", methods=["GET", "POST"], function_name="SamFunc1"), + Route(path="/path2", methods=["PUT", "GET"], function_name="SamFunc1"), + Route(path="/path3", methods=["DELETE"], function_name="SamFunc1"), ] - def test_with_no_apis(self): + def test_with_no_routes(self): template = { "Resources": { - "Api1": { "Type": "AWS::Serverless::Api", - "Properties": { - "StageName": "Prod" - } + "Properties": {"StageName": "Prod"}, } } } - provider = SamApiProvider(template) + provider = ApiProvider(template) - self.assertEquals(len(provider.apis), 0) - self.assertEquals(provider.apis, []) + self.assertEquals(provider.routes, []) - def test_with_inline_swagger_apis(self): + def test_with_inline_swagger_routes(self): template = { "Resources": { - "Api1": { "Type": "AWS::Serverless::Api", "Properties": { "StageName": "Prod", - "DefinitionBody": make_swagger(self.input_apis) - } + "DefinitionBody": make_swagger(self.input_routes), + }, } } } - provider = SamApiProvider(template) - assertCountEqual(self, self.input_apis, provider.apis) + provider = ApiProvider(template) + assertCountEqual(self, self.input_routes, provider.routes) def test_with_swagger_as_local_file(self): - with tempfile.NamedTemporaryFile(mode='w') as fp: + with tempfile.NamedTemporaryFile(mode="w", delete=False) as fp: filename = fp.name - swagger = make_swagger(self.input_apis) + swagger = make_swagger(self.input_routes) json.dump(swagger, fp) fp.flush() template = { "Resources": { - "Api1": { "Type": "AWS::Serverless::Api", - "Properties": { - "StageName": "Prod", - "DefinitionUri": filename - } + "Properties": {"StageName": "Prod", "DefinitionUri": filename}, } } } - provider = SamApiProvider(template) - assertCountEqual(self, self.input_apis, provider.apis) + provider = ApiProvider(template) + assertCountEqual(self, self.input_routes, provider.routes) - @patch("samcli.commands.local.lib.sam_api_provider.SamSwaggerReader") - def test_with_swagger_as_both_body_and_uri(self, SamSwaggerReaderMock): + @patch("samcli.commands.local.lib.cfn_base_api_provider.SwaggerReader") + def test_with_swagger_as_both_body_and_uri_called(self, SwaggerReaderMock): body = {"some": "body"} filename = "somefile.txt" template = { "Resources": { - "Api1": { "Type": "AWS::Serverless::Api", "Properties": { "StageName": "Prod", "DefinitionUri": filename, - "DefinitionBody": body - } + "DefinitionBody": body, + }, } } } - SamSwaggerReaderMock.return_value.read.return_value = make_swagger(self.input_apis) + SwaggerReaderMock.return_value.read.return_value = make_swagger( + self.input_routes + ) cwd = "foo" - provider = SamApiProvider(template, cwd=cwd) - assertCountEqual(self, self.input_apis, provider.apis) - SamSwaggerReaderMock.assert_called_with(definition_body=body, definition_uri=filename, working_dir=cwd) + provider = ApiProvider(template, cwd=cwd) + assertCountEqual(self, self.input_routes, provider.routes) + SwaggerReaderMock.assert_called_with( + definition_body=body, definition_uri=filename, working_dir=cwd + ) def test_swagger_with_any_method(self): - apis = [ - Api(path="/path", method="any", function_name="SamFunc1", cors=None) - ] - - expected_apis = [ - Api(path="/path", method="GET", function_name="SamFunc1", cors=None, stage_name="Prod"), - Api(path="/path", method="POST", function_name="SamFunc1", cors=None, stage_name="Prod"), - Api(path="/path", method="PUT", function_name="SamFunc1", cors=None, stage_name="Prod"), - Api(path="/path", method="DELETE", function_name="SamFunc1", cors=None, stage_name="Prod"), - Api(path="/path", method="HEAD", function_name="SamFunc1", cors=None, stage_name="Prod"), - Api(path="/path", method="OPTIONS", function_name="SamFunc1", cors=None, stage_name="Prod"), - Api(path="/path", method="PATCH", function_name="SamFunc1", cors=None, stage_name="Prod") + routes = [Route(path="/path", methods=["any"], function_name="SamFunc1")] + + expected_routes = [ + Route( + path="/path", + methods=["GET", "DELETE", "PUT", "POST", "HEAD", "OPTIONS", "PATCH"], + function_name="SamFunc1", + ) ] template = { @@ -541,107 +438,97 @@ def test_swagger_with_any_method(self): "Type": "AWS::Serverless::Api", "Properties": { "StageName": "Prod", - "DefinitionBody": make_swagger(apis) - } + "DefinitionBody": make_swagger(routes), + }, } } } - provider = SamApiProvider(template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(template) + assertCountEqual(self, expected_routes, provider.routes) def test_with_binary_media_types(self): template = { "Resources": { - "Api1": { "Type": "AWS::Serverless::Api", "Properties": { "StageName": "Prod", - "DefinitionBody": make_swagger(self.input_apis, binary_media_types=self.binary_types) - } + "DefinitionBody": make_swagger( + self.input_routes, binary_media_types=self.binary_types + ), + }, } } } expected_binary_types = sorted(self.binary_types) - expected_apis = [ - Api(path="/path1", method="GET", function_name="SamFunc1", cors=None, - binary_media_types=expected_binary_types, stage_name="Prod"), - Api(path="/path1", method="POST", function_name="SamFunc1", cors=None, - binary_media_types=expected_binary_types, stage_name="Prod"), - - Api(path="/path2", method="PUT", function_name="SamFunc1", cors=None, - binary_media_types=expected_binary_types, stage_name="Prod"), - Api(path="/path2", method="GET", function_name="SamFunc1", cors=None, - binary_media_types=expected_binary_types, stage_name="Prod"), - - Api(path="/path3", method="DELETE", function_name="SamFunc1", cors=None, - binary_media_types=expected_binary_types, stage_name="Prod") + expected_routes = [ + Route(path="/path1", methods=["GET", "POST"], function_name="SamFunc1"), + Route(path="/path2", methods=["GET", "PUT"], function_name="SamFunc1"), + Route(path="/path3", methods=["DELETE"], function_name="SamFunc1"), ] - provider = SamApiProvider(template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(template) + assertCountEqual(self, expected_routes, provider.routes) + assertCountEqual(self, provider.api.binary_media_types, expected_binary_types) def test_with_binary_media_types_in_swagger_and_on_resource(self): - input_apis = [ - Api(path="/path", method="OPTIONS", function_name="SamFunc1", stage_name="Prod"), + input_routes = [ + Route(path="/path", methods=["OPTIONS"], function_name="SamFunc1") ] extra_binary_types = ["text/html"] template = { "Resources": { - "Api1": { "Type": "AWS::Serverless::Api", "Properties": { "BinaryMediaTypes": extra_binary_types, "StageName": "Prod", - "DefinitionBody": make_swagger(input_apis, binary_media_types=self.binary_types) - } + "DefinitionBody": make_swagger( + input_routes, binary_media_types=self.binary_types + ), + }, } } } expected_binary_types = sorted(self.binary_types + extra_binary_types) - expected_apis = [ - Api(path="/path", method="OPTIONS", function_name="SamFunc1", binary_media_types=expected_binary_types, - stage_name="Prod"), + expected_routes = [ + Route(path="/path", methods=["OPTIONS"], function_name="SamFunc1") ] - provider = SamApiProvider(template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(template) + assertCountEqual(self, expected_routes, provider.routes) + assertCountEqual(self, provider.api.binary_media_types, expected_binary_types) class TestSamApiProviderWithExplicitAndImplicitApis(TestCase): - def setUp(self): - self.explicit_apis = [ - Api(path="/path1", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), - Api(path="/path2", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), - Api(path="/path3", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod") + self.stage_name = "Prod" + self.explicit_routes = [ + Route(path="/path1", methods=["GET"], function_name="explicitfunction"), + Route(path="/path2", methods=["GET"], function_name="explicitfunction"), + Route(path="/path3", methods=["GET"], function_name="explicitfunction"), ] - self.swagger = make_swagger(self.explicit_apis) + self.swagger = make_swagger(self.explicit_routes) self.template = { "Resources": { - "Api1": { "Type": "AWS::Serverless::Api", - "Properties": { - "StageName": "Prod", - } + "Properties": {"StageName": "Prod"}, }, - "ImplicitFunc": { "Type": "AWS::Serverless::Function", "Properties": { "CodeUri": "/usr/foo/bar", "Runtime": "nodejs4.3", - "Handler": "index.handler" - } - } + "Handler": "index.handler", + }, + }, } } @@ -649,162 +536,153 @@ def test_must_union_implicit_and_explicit(self): events = { "Event1": { "Type": "Api", - "Properties": { - "Path": "/path1", - "Method": "POST" - } + "Properties": {"Path": "/path1", "Method": "POST"}, }, - "Event2": { "Type": "Api", - "Properties": { - "Path": "/path2", - "Method": "POST" - } + "Properties": {"Path": "/path2", "Method": "POST"}, }, - "Event3": { "Type": "Api", - "Properties": { - "Path": "/path3", - "Method": "POST" - } - } + "Properties": {"Path": "/path3", "Method": "POST"}, + }, } - self.template["Resources"]["Api1"]["Properties"]["DefinitionBody"] = self.swagger + self.template["Resources"]["Api1"]["Properties"][ + "DefinitionBody" + ] = self.swagger self.template["Resources"]["ImplicitFunc"]["Properties"]["Events"] = events - expected_apis = [ + expected_routes = [ # From Explicit APIs - Api(path="/path1", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), - Api(path="/path2", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), - Api(path="/path3", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), + Route(path="/path1", methods=["GET"], function_name="explicitfunction"), + Route(path="/path2", methods=["GET"], function_name="explicitfunction"), + Route(path="/path3", methods=["GET"], function_name="explicitfunction"), # From Implicit APIs - Api(path="/path1", method="POST", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path2", method="POST", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path3", method="POST", function_name="ImplicitFunc", cors=None, stage_name="Prod") + Route(path="/path1", methods=["POST"], function_name="ImplicitFunc"), + Route(path="/path2", methods=["POST"], function_name="ImplicitFunc"), + Route(path="/path3", methods=["POST"], function_name="ImplicitFunc"), ] - provider = SamApiProvider(self.template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(self.template) + assertCountEqual(self, expected_routes, provider.routes) def test_must_prefer_implicit_api_over_explicit(self): - implicit_apis = { + implicit_routes = { "Event1": { "Type": "Api", "Properties": { # This API is duplicated between implicit & explicit "Path": "/path1", - "Method": "get" - } + "Method": "get", + }, }, - "Event2": { "Type": "Api", - "Properties": { - "Path": "/path2", - "Method": "POST" - } - } + "Properties": {"Path": "/path2", "Method": "POST"}, + }, } - self.template["Resources"]["Api1"]["Properties"]["DefinitionBody"] = self.swagger - self.template["Resources"]["ImplicitFunc"]["Properties"]["Events"] = implicit_apis + self.template["Resources"]["Api1"]["Properties"][ + "DefinitionBody" + ] = self.swagger + self.template["Resources"]["ImplicitFunc"]["Properties"][ + "Events" + ] = implicit_routes - expected_apis = [ - Api(path="/path1", method="GET", function_name="ImplicitFunc", stage_name="Prod"), + expected_routes = [ + Route(path="/path1", methods=["GET"], function_name="ImplicitFunc"), # Comes from Implicit - - Api(path="/path2", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), - Api(path="/path2", method="POST", function_name="ImplicitFunc", stage_name="Prod"), + Route(path="/path2", methods=["GET"], function_name="explicitfunction"), + Route(path="/path2", methods=["POST"], function_name="ImplicitFunc"), # Comes from implicit - - Api(path="/path3", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), + Route(path="/path3", methods=["GET"], function_name="explicitfunction"), ] - provider = SamApiProvider(self.template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(self.template) + assertCountEqual(self, expected_routes, provider.routes) def test_must_prefer_implicit_with_any_method(self): - implicit_apis = { + implicit_routes = { "Event1": { "Type": "Api", "Properties": { # This API is duplicated between implicit & explicit "Path": "/path", - "Method": "ANY" - } + "Method": "ANY", + }, } } - explicit_apis = [ + explicit_routes = [ # Explicit should be over masked completely by implicit, because of "ANY" - Api(path="/path", method="GET", function_name="explicitfunction", cors=None), - Api(path="/path", method="DELETE", function_name="explicitfunction", cors=None), + Route(path="/path", methods=["GET"], function_name="explicitfunction"), + Route(path="/path", methods=["DELETE"], function_name="explicitfunction"), ] - self.template["Resources"]["Api1"]["Properties"]["DefinitionBody"] = make_swagger(explicit_apis) - self.template["Resources"]["ImplicitFunc"]["Properties"]["Events"] = implicit_apis - - expected_apis = [ - Api(path="/path", method="GET", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="POST", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="PUT", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="DELETE", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="HEAD", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="OPTIONS", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="PATCH", function_name="ImplicitFunc", cors=None, stage_name="Prod") + self.template["Resources"]["Api1"]["Properties"][ + "DefinitionBody" + ] = make_swagger(explicit_routes) + self.template["Resources"]["ImplicitFunc"]["Properties"][ + "Events" + ] = implicit_routes + + expected_routes = [ + Route( + path="/path", + methods=["GET", "DELETE", "PUT", "POST", "HEAD", "OPTIONS", "PATCH"], + function_name="ImplicitFunc", + ) ] - provider = SamApiProvider(self.template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(self.template) + assertCountEqual(self, expected_routes, provider.routes) def test_with_any_method_on_both(self): - implicit_apis = { + implicit_routes = { "Event1": { "Type": "Api", "Properties": { # This API is duplicated between implicit & explicit "Path": "/path", - "Method": "ANY" - } + "Method": "ANY", + }, }, "Event2": { "Type": "Api", "Properties": { # This API is duplicated between implicit & explicit "Path": "/path2", - "Method": "GET" - } - } + "Method": "GET", + }, + }, } - explicit_apis = [ + explicit_routes = [ # Explicit should be over masked completely by implicit, because of "ANY" - Api(path="/path", method="ANY", function_name="explicitfunction", cors=None), - Api(path="/path2", method="POST", function_name="explicitfunction", cors=None), + Route(path="/path", methods=["ANY"], function_name="explicitfunction"), + Route(path="/path2", methods=["POST"], function_name="explicitfunction"), ] - self.template["Resources"]["Api1"]["Properties"]["DefinitionBody"] = make_swagger(explicit_apis) - self.template["Resources"]["ImplicitFunc"]["Properties"]["Events"] = implicit_apis - - expected_apis = [ - Api(path="/path", method="GET", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="POST", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="PUT", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="DELETE", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="HEAD", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="OPTIONS", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path", method="PATCH", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - - Api(path="/path2", method="GET", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/path2", method="POST", function_name="explicitfunction", cors=None, stage_name="Prod") + self.template["Resources"]["Api1"]["Properties"][ + "DefinitionBody" + ] = make_swagger(explicit_routes) + self.template["Resources"]["ImplicitFunc"]["Properties"][ + "Events" + ] = implicit_routes + + expected_routes = [ + Route( + path="/path", + methods=["GET", "DELETE", "PUT", "POST", "HEAD", "OPTIONS", "PATCH"], + function_name="ImplicitFunc", + ), + Route(path="/path2", methods=["GET"], function_name="ImplicitFunc"), + Route(path="/path2", methods=["POST"], function_name="explicitfunction"), ] - provider = SamApiProvider(self.template) - print(provider.apis) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(self.template) + assertCountEqual(self, expected_routes, provider.routes) def test_must_add_explicit_api_when_ref_with_rest_api_id(self): events = { @@ -813,90 +691,89 @@ def test_must_add_explicit_api_when_ref_with_rest_api_id(self): "Properties": { "Path": "/newpath1", "Method": "POST", - "RestApiId": "Api1" # This path must get added to this API - } + "RestApiId": "Api1", # This path must get added to this API + }, }, - "Event2": { "Type": "Api", "Properties": { "Path": "/newpath2", "Method": "POST", - "RestApiId": {"Ref": "Api1"} # This path must get added to this API - } - } + "RestApiId": { + "Ref": "Api1" + }, # This path must get added to this API + }, + }, } - self.template["Resources"]["Api1"]["Properties"]["DefinitionBody"] = self.swagger + self.template["Resources"]["Api1"]["Properties"][ + "DefinitionBody" + ] = self.swagger self.template["Resources"]["ImplicitFunc"]["Properties"]["Events"] = events - expected_apis = [ + expected_routes = [ # From Explicit APIs - Api(path="/path1", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), - Api(path="/path2", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), - Api(path="/path3", method="GET", function_name="explicitfunction", cors=None, stage_name="Prod"), + Route(path="/path1", methods=["GET"], function_name="explicitfunction"), + Route(path="/path2", methods=["GET"], function_name="explicitfunction"), + Route(path="/path3", methods=["GET"], function_name="explicitfunction"), # From Implicit APIs - Api(path="/newpath1", method="POST", function_name="ImplicitFunc", cors=None, stage_name="Prod"), - Api(path="/newpath2", method="POST", function_name="ImplicitFunc", cors=None, stage_name="Prod") + Route(path="/newpath1", methods=["POST"], function_name="ImplicitFunc"), + Route(path="/newpath2", methods=["POST"], function_name="ImplicitFunc"), ] - provider = SamApiProvider(self.template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(self.template) + assertCountEqual(self, expected_routes, provider.routes) - def test_both_apis_must_get_binary_media_types(self): + def test_both_routes_must_get_binary_media_types(self): events = { "Event1": { "Type": "Api", - "Properties": { - "Path": "/newpath1", - "Method": "POST" - } + "Properties": {"Path": "/newpath1", "Method": "POST"}, }, - "Event2": { "Type": "Api", - "Properties": { - "Path": "/newpath2", - "Method": "POST" - } - } + "Properties": {"Path": "/newpath2", "Method": "POST"}, + }, } # Binary type for implicit self.template["Globals"] = { - "Api": { - "BinaryMediaTypes": ["image~1gif", "image~1png"] - } + "Api": {"BinaryMediaTypes": ["image~1gif", "image~1png"]} } self.template["Resources"]["ImplicitFunc"]["Properties"]["Events"] = events - self.template["Resources"]["Api1"]["Properties"]["DefinitionBody"] = self.swagger + self.template["Resources"]["Api1"]["Properties"][ + "DefinitionBody" + ] = self.swagger # Binary type for explicit - self.template["Resources"]["Api1"]["Properties"]["BinaryMediaTypes"] = ["explicit/type1", "explicit/type2"] + self.template["Resources"]["Api1"]["Properties"]["BinaryMediaTypes"] = [ + "explicit/type1", + "explicit/type2", + ] # Because of Globals, binary types will be concatenated on the explicit API - expected_explicit_binary_types = ["explicit/type1", "explicit/type2", "image/gif", "image/png"] - expected_implicit_binary_types = ["image/gif", "image/png"] + expected_explicit_binary_types = [ + "explicit/type1", + "explicit/type2", + "image/gif", + "image/png", + ] - expected_apis = [ + expected_routes = [ # From Explicit APIs - Api(path="/path1", method="GET", function_name="explicitfunction", - binary_media_types=expected_explicit_binary_types, stage_name="Prod"), - Api(path="/path2", method="GET", function_name="explicitfunction", - binary_media_types=expected_explicit_binary_types, stage_name="Prod"), - Api(path="/path3", method="GET", function_name="explicitfunction", - binary_media_types=expected_explicit_binary_types, stage_name="Prod"), + Route(path="/path1", methods=["GET"], function_name="explicitfunction"), + Route(path="/path2", methods=["GET"], function_name="explicitfunction"), + Route(path="/path3", methods=["GET"], function_name="explicitfunction"), # From Implicit APIs - Api(path="/newpath1", method="POST", function_name="ImplicitFunc", - binary_media_types=expected_implicit_binary_types, - stage_name="Prod"), - Api(path="/newpath2", method="POST", function_name="ImplicitFunc", - binary_media_types=expected_implicit_binary_types, - stage_name="Prod") + Route(path="/newpath1", methods=["POST"], function_name="ImplicitFunc"), + Route(path="/newpath2", methods=["POST"], function_name="ImplicitFunc"), ] - provider = SamApiProvider(self.template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(self.template) + assertCountEqual(self, expected_routes, provider.routes) + assertCountEqual( + self, provider.api.binary_media_types, expected_explicit_binary_types + ) def test_binary_media_types_with_rest_api_id_reference(self): events = { @@ -905,66 +782,70 @@ def test_binary_media_types_with_rest_api_id_reference(self): "Properties": { "Path": "/connected-to-explicit-path", "Method": "POST", - "RestApiId": "Api1" - } + "RestApiId": "Api1", + }, }, - "Event2": { "Type": "Api", - "Properties": { - "Path": "/true-implicit-path", - "Method": "POST" - } - } + "Properties": {"Path": "/true-implicit-path", "Method": "POST"}, + }, } # Binary type for implicit self.template["Globals"] = { - "Api": { - "BinaryMediaTypes": ["image~1gif", "image~1png"] - } + "Api": {"BinaryMediaTypes": ["image~1gif", "image~1png"]} } self.template["Resources"]["ImplicitFunc"]["Properties"]["Events"] = events - self.template["Resources"]["Api1"]["Properties"]["DefinitionBody"] = self.swagger + self.template["Resources"]["Api1"]["Properties"][ + "DefinitionBody" + ] = self.swagger # Binary type for explicit - self.template["Resources"]["Api1"]["Properties"]["BinaryMediaTypes"] = ["explicit/type1", "explicit/type2"] + self.template["Resources"]["Api1"]["Properties"]["BinaryMediaTypes"] = [ + "explicit/type1", + "explicit/type2", + ] # Because of Globals, binary types will be concatenated on the explicit API - expected_explicit_binary_types = ["explicit/type1", "explicit/type2", "image/gif", "image/png"] - expected_implicit_binary_types = ["image/gif", "image/png"] + expected_explicit_binary_types = [ + "explicit/type1", + "explicit/type2", + "image/gif", + "image/png", + ] + # expected_implicit_binary_types = ["image/gif", "image/png"] - expected_apis = [ + expected_routes = [ # From Explicit APIs - Api(path="/path1", method="GET", function_name="explicitfunction", - binary_media_types=expected_explicit_binary_types, stage_name="Prod"), - Api(path="/path2", method="GET", function_name="explicitfunction", - binary_media_types=expected_explicit_binary_types, stage_name="Prod"), - Api(path="/path3", method="GET", function_name="explicitfunction", - binary_media_types=expected_explicit_binary_types, stage_name="Prod"), - + Route(path="/path1", methods=["GET"], function_name="explicitfunction"), + Route(path="/path2", methods=["GET"], function_name="explicitfunction"), + Route(path="/path3", methods=["GET"], function_name="explicitfunction"), # Because of the RestApiId, Implicit APIs will also get the binary media types inherited from # the corresponding Explicit API - Api(path="/connected-to-explicit-path", method="POST", function_name="ImplicitFunc", - binary_media_types=expected_explicit_binary_types, - stage_name="Prod"), - + Route( + path="/connected-to-explicit-path", + methods=["POST"], + function_name="ImplicitFunc", + ), # This is still just a true implicit API because it does not have RestApiId property - Api(path="/true-implicit-path", method="POST", function_name="ImplicitFunc", - binary_media_types=expected_implicit_binary_types, - stage_name="Prod") + Route( + path="/true-implicit-path", + methods=["POST"], + function_name="ImplicitFunc", + ), ] - provider = SamApiProvider(self.template) - assertCountEqual(self, expected_apis, provider.apis) + provider = ApiProvider(self.template) + assertCountEqual(self, expected_routes, provider.routes) + assertCountEqual( + self, provider.api.binary_media_types, expected_explicit_binary_types + ) class TestSamStageValues(TestCase): - def test_provider_parse_stage_name(self): template = { "Resources": { - "TestApi": { "Type": "AWS::Serverless::Api", "Properties": { @@ -978,39 +859,35 @@ def test_provider_parse_stage_name(self): "type": "aws_proxy", "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" - "/functions/${NoApiEventFunction.Arn}/invocations", + "/functions/${NoApiEventFunction.Arn}/invocations" }, "responses": {}, - }, + } } } - } - } - } + }, + }, } } } - provider = SamApiProvider(template) - api1 = Api(path='/path', method='GET', function_name='NoApiEventFunction', cors=None, binary_media_types=[], - stage_name='dev', - stage_variables=None) + provider = ApiProvider(template) + route1 = Route( + path="/path", methods=["GET"], function_name="NoApiEventFunction" + ) - self.assertIn(api1, provider.apis) + self.assertIn(route1, provider.routes) + self.assertEquals(provider.api.stage_name, "dev") + self.assertEquals(provider.api.stage_variables, None) def test_provider_stage_variables(self): template = { "Resources": { - "TestApi": { "Type": "AWS::Serverless::Api", "Properties": { "StageName": "dev", - "Variables": { - "vis": "data", - "random": "test", - "foo": "bar" - }, + "Variables": {"vis": "data", "random": "test", "foo": "bar"}, "DefinitionBody": { "paths": { "/path": { @@ -1020,140 +897,506 @@ def test_provider_stage_variables(self): "type": "aws_proxy", "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" - "/functions/${NoApiEventFunction.Arn}/invocations", + "/functions/${NoApiEventFunction.Arn}/invocations" }, "responses": {}, - }, + } } } + } + }, + }, + } + } + } + provider = ApiProvider(template) + route1 = Route( + path="/path", methods=["GET"], function_name="NoApiEventFunction" + ) + + self.assertIn(route1, provider.routes) + self.assertEquals(provider.api.stage_name, "dev") + self.assertEquals( + provider.api.stage_variables, + {"vis": "data", "random": "test", "foo": "bar"}, + ) + def test_multi_stage_get_all(self): + template = OrderedDict({"Resources": {}}) + template["Resources"]["TestApi"] = { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "dev", + "Variables": {"vis": "data", "random": "test", "foo": "bar"}, + "DefinitionBody": { + "paths": { + "/path2": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } } } } + }, + }, + } + + template["Resources"]["ProductionApi"] = { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Production", + "Variables": {"vis": "prod data", "random": "test", "foo": "bar"}, + "DefinitionBody": { + "paths": { + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, + "/anotherpath": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, + } + }, + }, + } + + provider = ApiProvider(template) + + result = [f for f in provider.get_all()] + routes = result[0].routes + + route1 = Route( + path="/path2", methods=["GET"], function_name="NoApiEventFunction" + ) + route2 = Route( + path="/path", methods=["GET"], function_name="NoApiEventFunction" + ) + route3 = Route( + path="/anotherpath", methods=["POST"], function_name="NoApiEventFunction" + ) + self.assertEquals(len(routes), 3) + self.assertIn(route1, routes) + self.assertIn(route2, routes) + self.assertIn(route3, routes) + + self.assertEquals(provider.api.stage_name, "Production") + self.assertEquals( + provider.api.stage_variables, + {"vis": "prod data", "random": "test", "foo": "bar"}, + ) + + +class TestSamCors(TestCase): + def test_provider_parse_cors_string(self): + template = { + "Resources": { + "TestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Cors": "*", + "DefinitionBody": { + "paths": { + "/path2": { + "post": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, + } + }, + }, } } } - provider = SamApiProvider(template) - api1 = Api(path='/path', method='GET', function_name='NoApiEventFunction', cors=None, binary_media_types=[], - stage_name='dev', - stage_variables={ - "vis": "data", - "random": "test", - "foo": "bar" - }) - - self.assertIn(api1, provider.apis) - def test_multi_stage_get_all(self): + provider = ApiProvider(template) + + routes = provider.routes + cors = Cors( + allow_origin="*", + allow_methods=",".join( + sorted(["GET", "DELETE", "PUT", "POST", "HEAD", "OPTIONS", "PATCH"]) + ), + ) + route1 = Route( + path="/path2", + methods=["POST", "OPTIONS"], + function_name="NoApiEventFunction", + ) + route2 = Route( + path="/path", methods=["GET", "OPTIONS"], function_name="NoApiEventFunction" + ) + + self.assertEquals(len(routes), 2) + self.assertIn(route1, routes) + self.assertIn(route2, routes) + self.assertEquals(provider.api.cors, cors) + + def test_provider_parse_cors_dict(self): template = { "Resources": { "TestApi": { "Type": "AWS::Serverless::Api", "Properties": { - "StageName": "dev", - "Variables": { - "vis": "data", - "random": "test", - "foo": "bar" + "StageName": "Prod", + "Cors": { + "AllowMethods": "POST, GET", + "AllowOrigin": "*", + "AllowHeaders": "Upgrade-Insecure-Requests", + "MaxAge": 600, }, "DefinitionBody": { "paths": { "/path2": { - "get": { + "post": { "x-amazon-apigateway-integration": { - "httpMethod": "POST", "type": "aws_proxy", "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" - "/functions/${NoApiEventFunction.Arn}/invocations", + "/functions/${NoApiEventFunction.Arn}/invocations" }, "responses": {}, - }, + } } - } + }, + "/path": { + "post": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, } - } - } - }, - "ProductionApi": { + }, + }, + } + } + } + + provider = ApiProvider(template) + + routes = provider.routes + cors = Cors( + allow_origin="*", + allow_methods=",".join(sorted(["POST", "GET", "OPTIONS"])), + allow_headers="Upgrade-Insecure-Requests", + max_age=600, + ) + route1 = Route( + path="/path2", + methods=["POST", "OPTIONS"], + function_name="NoApiEventFunction", + ) + route2 = Route( + path="/path", + methods=["POST", "OPTIONS"], + function_name="NoApiEventFunction", + ) + + self.assertEquals(len(routes), 2) + self.assertIn(route1, routes) + self.assertIn(route2, routes) + self.assertEquals(provider.api.cors, cors) + + def test_provider_parse_cors_dict_star_allow(self): + template = { + "Resources": { + "TestApi": { "Type": "AWS::Serverless::Api", "Properties": { - "StageName": "Production", - "Variables": { - "vis": "prod data", - "random": "test", - "foo": "bar" + "StageName": "Prod", + "Cors": { + "AllowMethods": "*", + "AllowOrigin": "*", + "AllowHeaders": "Upgrade-Insecure-Requests", + "MaxAge": 600, }, "DefinitionBody": { "paths": { + "/path2": { + "post": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, "/path": { - "get": { + "post": { "x-amazon-apigateway-integration": { - "httpMethod": "POST", "type": "aws_proxy", "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" - "/functions/${NoApiEventFunction.Arn}/invocations", + "/functions/${NoApiEventFunction.Arn}/invocations" }, "responses": {}, - }, + } } }, - "/anotherpath": { + } + }, + }, + } + } + } + + provider = ApiProvider(template) + + routes = provider.routes + cors = Cors( + allow_origin="*", + allow_methods=",".join(sorted(Route.ANY_HTTP_METHODS)), + allow_headers="Upgrade-Insecure-Requests", + max_age=600, + ) + route1 = Route( + path="/path2", + methods=["POST", "OPTIONS"], + function_name="NoApiEventFunction", + ) + route2 = Route( + path="/path", + methods=["POST", "OPTIONS"], + function_name="NoApiEventFunction", + ) + + self.assertEquals(len(routes), 2) + self.assertIn(route1, routes) + self.assertIn(route2, routes) + self.assertEquals(provider.api.cors, cors) + + def test_invalid_cors_dict_allow_methods(self): + template = { + "Resources": { + "TestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Cors": { + "AllowMethods": "GET, INVALID_METHOD", + "AllowOrigin": "*", + "AllowHeaders": "Upgrade-Insecure-Requests", + "MaxAge": 600, + }, + "DefinitionBody": { + "paths": { + "/path2": { "post": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, + "/path": { + "post": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, + } + }, + }, + } + } + } + with self.assertRaises( + InvalidSamDocumentException, + msg="ApiProvider should fail for Invalid Cors Allow method", + ): + ApiProvider(template) + + def test_default_cors_dict_prop(self): + template = { + "Resources": { + "TestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Cors": {"AllowOrigin": "www.domain.com"}, + "DefinitionBody": { + "paths": { + "/path2": { + "get": { "x-amazon-apigateway-integration": { "httpMethod": "POST", "type": "aws_proxy", "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" - "/functions/${NoApiEventFunction.Arn}/invocations", + "/functions/${NoApiEventFunction.Arn}/invocations" }, "responses": {}, - }, + } } } - } - } - } + }, + }, } } } - provider = SamApiProvider(template) - - result = [f for f in provider.get_all()] + provider = ApiProvider(template) + + routes = provider.routes + cors = Cors( + allow_origin="www.domain.com", + allow_methods=",".join(sorted(Route.ANY_HTTP_METHODS)), + ) + route1 = Route( + path="/path2", + methods=["GET", "OPTIONS"], + function_name="NoApiEventFunction", + ) + self.assertEquals(len(routes), 1) + self.assertIn(route1, routes) + self.assertEquals(provider.api.cors, cors) + + def test_global_cors(self): + template = { + "Globals": { + "Api": { + "Cors": { + "AllowMethods": "GET", + "AllowOrigin": "*", + "AllowHeaders": "Upgrade-Insecure-Requests", + "MaxAge": 600, + } + } + }, + "Resources": { + "TestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "DefinitionBody": { + "paths": { + "/path2": { + "get": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, + "/path": { + "get": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31" + "/functions/${NoApiEventFunction.Arn}/invocations" + }, + "responses": {}, + } + } + }, + } + }, + }, + } + }, + } - api1 = Api(path='/path2', method='GET', function_name='NoApiEventFunction', cors=None, binary_media_types=[], - stage_name='dev', - stage_variables={ - "vis": "data", - "random": "test", - "foo": "bar" - }) - api2 = Api(path='/path', method='GET', function_name='NoApiEventFunction', cors=None, binary_media_types=[], - stage_name='Production', stage_variables={'vis': 'prod data', 'random': 'test', 'foo': 'bar'}) - api3 = Api(path='/anotherpath', method='POST', function_name='NoApiEventFunction', cors=None, - binary_media_types=[], - stage_name='Production', - stage_variables={ - "vis": "prod data", - "random": "test", - "foo": "bar" - }) - self.assertEquals(len(result), 3) - self.assertIn(api1, result) - self.assertIn(api2, result) - self.assertIn(api3, result) - - -def make_swagger(apis, binary_media_types=None): + provider = ApiProvider(template) + + routes = provider.routes + cors = Cors( + allow_origin="*", + allow_headers="Upgrade-Insecure-Requests", + allow_methods=",".join(["GET", "OPTIONS"]), + max_age=600, + ) + route1 = Route( + path="/path2", + methods=["GET", "OPTIONS"], + function_name="NoApiEventFunction", + ) + route2 = Route( + path="/path", methods=["GET", "OPTIONS"], function_name="NoApiEventFunction" + ) + + self.assertEquals(len(routes), 2) + self.assertIn(route1, routes) + self.assertIn(route2, routes) + self.assertEquals(provider.api.cors, cors) + + +def make_swagger(routes, binary_media_types=None): """ Given a list of API configurations named tuples, returns a Swagger document Parameters ---------- - apis : list of samcli.commands.local.lib.provider.Api + routes : list of samcli.commands.local.agiw.local_agiw_service.Route binary_media_types : list of str Returns @@ -1162,28 +1405,25 @@ def make_swagger(apis, binary_media_types=None): Swagger document """ - swagger = { - "paths": { - } - } + swagger = {"paths": {}} - for api in apis: + for api in routes: swagger["paths"].setdefault(api.path, {}) integration = { "x-amazon-apigateway-integration": { "type": "aws_proxy", "uri": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1" - ":123456789012:function:{}/invocations".format( - api.function_name) # NOQA + ":123456789012:function:{}/invocations".format( + api.function_name + ), # NOQA } } + for method in api.methods: + if method.lower() == "any": + method = "x-amazon-apigateway-any-method" - method = api.method - if method.lower() == "any": - method = "x-amazon-apigateway-any-method" - - swagger["paths"][api.path][method] = integration + swagger["paths"][api.path][method] = integration if binary_media_types: swagger["x-amazon-apigateway-binary-media-types"] = binary_media_types diff --git a/tests/unit/commands/local/lib/test_sam_base_provider.py b/tests/unit/commands/local/lib/test_sam_base_provider.py index c9d870e95a..b516437948 100644 --- a/tests/unit/commands/local/lib/test_sam_base_provider.py +++ b/tests/unit/commands/local/lib/test_sam_base_provider.py @@ -1,201 +1,27 @@ - from unittest import TestCase from mock import Mock, patch -from nose_parameterized import parameterized - from samcli.commands.local.lib.sam_base_provider import SamBaseProvider - - -class TestSamBaseProvider_resolve_parameters(TestCase): - - @parameterized.expand([ - ("AWS::AccountId", "123456789012"), - ("AWS::Partition", "aws"), - ("AWS::Region", "us-east-1"), - ("AWS::StackName", "local"), - ("AWS::StackId", "arn:aws:cloudformation:us-east-1:123456789012:stack/" - "local/51af3dc0-da77-11e4-872e-1234567db123"), - ("AWS::URLSuffix", "localhost"), - ]) - def test_with_pseudo_parameters(self, parameter, expected_value): - - template_dict = { - "Key": { - "Ref": parameter - } - } - - expected_template = { - "Key": expected_value - } - - result = SamBaseProvider._resolve_parameters(template_dict, {}) - self.assertEquals(result, expected_template) - - def test_override_pseudo_parameters(self): - template = { - "Key": { - "Ref": "AWS::Region" - } - } - - override = { - "AWS::Region": "someregion" - } - - expected_template = { - "Key": "someregion" - } - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_parameter_with_defaults(self): - override = {} # No overrides - - template = { - "Parameters": { - "Key1": { - "Default": "Value1" - }, - "Key2": { - "Default": "Value2" - }, - "NoDefaultKey3": {} # No Default - }, - - "Resources": { - "R1": {"Ref": "Key1"}, - "R2": {"Ref": "Key2"}, - "R3": {"Ref": "NoDefaultKey3"} - } - } - - expected_template = { - "Parameters": { - "Key1": { - "Default": "Value1" - }, - "Key2": { - "Default": "Value2" - }, - "NoDefaultKey3": {} # No Default - }, - - "Resources": { - "R1": "Value1", - "R2": "Value2", - "R3": {"Ref": "NoDefaultKey3"} # No default value. so no subsitution - } - } - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_override_parameters(self): - template = { - "Parameters": { - "Key1": { - "Default": "Value1" - }, - "Key2": { - "Default": "Value2" - }, - "NoDefaultKey3": {}, - - "NoOverrideKey4": {} # No override Value provided - }, - - "Resources": { - "R1": {"Ref": "Key1"}, - "R2": {"Ref": "Key2"}, - "R3": {"Ref": "NoDefaultKey3"}, - "R4": {"Ref": "NoOverrideKey4"} - } - } - - override = { - "Key1": "OverrideValue1", - "Key2": "OverrideValue2", - "NoDefaultKey3": "OverrideValue3" - } - - expected_template = { - "Parameters": { - "Key1": { - "Default": "Value1" - }, - "Key2": { - "Default": "Value2" - }, - "NoDefaultKey3": {}, - "NoOverrideKey4": {} # No override Value provided - }, - - "Resources": { - "R1": "OverrideValue1", - "R2": "OverrideValue2", - "R3": "OverrideValue3", - "R4": {"Ref": "NoOverrideKey4"} - } - } - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_must_skip_non_ref_intrinsics(self): - template = { - "Key1": {"Fn::Sub": ["${AWS::Region}"]}, # Sub is not implemented - "Key2": {"Ref": "MyParam"} - } - - override = {"MyParam": "MyValue"} - - expected_template = { - "Key1": {"Fn::Sub": ["${AWS::Region}"]}, - "Key2": "MyValue" - } - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_must_skip_empty_overrides(self): - template = {"Key": {"Ref": "Param"}} - override = None - expected_template = {"Key": {"Ref": "Param"}} - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) - - def test_must_skip_empty_template(self): - template = {} - override = None - expected_template = {} - - self.assertEquals(SamBaseProvider._resolve_parameters(template, override), - expected_template) +from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver class TestSamBaseProvider_get_template(TestCase): - @patch("samcli.commands.local.lib.sam_base_provider.ResourceMetadataNormalizer") @patch("samcli.commands.local.lib.sam_base_provider.SamTranslatorWrapper") - @patch.object(SamBaseProvider, "_resolve_parameters") - def test_must_run_translator_plugins(self, - resolve_params_mock, - SamTranslatorWrapperMock, - resource_metadata_normalizer_patch): + @patch.object(IntrinsicResolver, "resolve_template") + def test_must_run_translator_plugins( + self, + resolve_template_mock, + SamTranslatorWrapperMock, + resource_metadata_normalizer_patch, + ): + resource_metadata_normalizer_patch.normalize.return_value = True + resolve_template_mock.return_value = {} translator_instance = SamTranslatorWrapperMock.return_value = Mock() - parameter_resolved_template = {"Key": "Value", "Parameter": "Resolved"} - resolve_params_mock.return_value = parameter_resolved_template - template = {"Key": "Value"} - overrides = {'some': 'value'} + overrides = {"some": "value"} SamBaseProvider.get_template(template, overrides) SamTranslatorWrapperMock.assert_called_once_with(template) translator_instance.run_plugins.assert_called_once() - resolve_params_mock.assert_called_once() - resource_metadata_normalizer_patch.normalize.assert_called_once_with(parameter_resolved_template) diff --git a/tests/unit/commands/validate/lib/test_sam_template_validator.py b/tests/unit/commands/validate/lib/test_sam_template_validator.py index 66bd659309..b8a485b743 100644 --- a/tests/unit/commands/validate/lib/test_sam_template_validator.py +++ b/tests/unit/commands/validate/lib/test_sam_template_validator.py @@ -174,8 +174,6 @@ def test_replace_local_codeuri_when_no_codeuri_given(self): # check template tempalte_resources = validator.sam_template.get("Resources") - self.assertEquals(tempalte_resources.get("ServerlessApi").get("Properties").get("DefinitionUri"), - "s3://bucket/value") self.assertEquals(tempalte_resources.get("ServerlessFunction").get("Properties").get("CodeUri"), "s3://bucket/value") diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index 8450af2e51..03aca7a436 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -6,6 +6,12 @@ from unittest import TestCase from mock import Mock, call, patch +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + + from samcli.lib.build.app_builder import ApplicationBuilder,\ UnsupportedBuilderLibraryVersionError, BuildError, \ LambdaBuilderError, ContainerBuildNotSupported @@ -131,9 +137,9 @@ def test_must_build_in_process(self, osutils_mock, get_workflow_config_mock): self.builder._build_function_in_process = Mock() - code_dir = "/base/dir/path/to/source" - artifacts_dir = "/build/dir/function_name" - manifest_path = os.path.join(code_dir, config_mock.manifest_name) + code_dir = str(Path("/base/dir/path/to/source").resolve()) + artifacts_dir = str(Path("/build/dir/function_name")) + manifest_path = str(Path(os.path.join(code_dir, config_mock.manifest_name)).resolve()) self.builder._build_function(function_name, codeuri, runtime) @@ -159,9 +165,9 @@ def test_must_build_in_container(self, osutils_mock, get_workflow_config_mock): self.builder._build_function_on_container = Mock() - code_dir = "/base/dir/path/to/source" - artifacts_dir = "/build/dir/function_name" - manifest_path = os.path.join(code_dir, config_mock.manifest_name) + code_dir = str(Path("/base/dir/path/to/source").resolve()) + artifacts_dir = str(Path("/build/dir/function_name")) + manifest_path = str(Path(os.path.join(code_dir, config_mock.manifest_name)).resolve()) # Settting the container manager will make us use the container self.builder._container_manager = Mock() diff --git a/tests/unit/lib/intrinsic_resolver/__init__.py b/tests/unit/lib/intrinsic_resolver/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_intrinsic_template_resolution.json b/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_intrinsic_template_resolution.json new file mode 100644 index 0000000000..0ec54a824a --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_intrinsic_template_resolution.json @@ -0,0 +1,139 @@ +{ + "Mappings": { + "TopLevel": { + "SecondLevelKey": { + "key": "https://s3location/" + } + } + }, + "Conditions": { + "ComplexCondition": { + "Fn::And": [ + { + "Fn::Equals": [ + { + "Fn::Or": [ + { + "Condition": "NotTestCondition" + }, + { + "Condition": "TestCondition" + } + ] + }, + false + ] + }, + true, + { + "Fn::If": ["TestCondition", true, false] + } + ] + }, + "TestCondition": { + "Fn::Equals": [ + { + "Ref": "EnvironmentType" + }, + "prod" + ] + }, + "NotTestCondition": { + "Fn::Not": [ + { + "Condition": "TestCondition" + } + ] + }, + "InvalidCondition": ["random items"] + }, + "Resources": { + "ReferenceLambdaLayerVersionLambdaFunction": { + "Properties": { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "MyCustomLambdaLayer" }] + }, + "Type": "AWS::Serverless::Function" + }, + "MyCustomLambdaLayer": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": "custom_layer/" + } + }, + "RestApi.Deployment": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "Fn::Base64": { + "Fn::Join": [ + ";", + { + "Fn::Split": [ + ",", + { + "Fn::Join": [",", ["a", "e", "f", "d"]] + } + ] + } + ] + } + }, + "BodyS3Location": { + "Fn::FindInMap": ["TopLevel", "SecondLevelKey", "key"] + } + } + }, + "RestApiResource": { + "Properties": { + "parentId": { + "Fn::GetAtt": ["RestApi.Deployment", "RootResourceId"] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "RestApi.Deployment" + } + } + }, + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "handler": "main.handle" + } + }, + "LambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": { + "Ref": "AWS::Region" + } + } + ] + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": ["HelloHandler2E4FBA4D", "Arn"] + }, + "/invocations" + ] + ] + } + } + } + } +} diff --git a/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_layers_resolution.json b/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_layers_resolution.json new file mode 100644 index 0000000000..f23194f779 --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_layers_resolution.json @@ -0,0 +1,174 @@ +{ + "Transform": "AWS::Serverless-2016-10-31", + "Parameters": + { + "LayerOneArn": + { + "Default": "arn:aws:lambda:us-west-2:111111111111:layer:layer:1", + "Type": "String" + }, + "LayerTwoArn": + { + "Default": "arn:aws:lambda:us-west-2:111111111111:layer:layer2:1", + "Type": "String" + }, + "ChangedLayerArn": + { + "Default": "arn:aws:lambda:us-west-2:111111111111:layer:changed_layer:1", + "Type": "String" + }, + "NonExistentLayerArn": + { + "Default": "arn:aws:lambda:us-west-2:111111111111:layer:non_existent_layer:1", + "Type": "String" + } + }, + "Resources": + { + "OneLayerVersionServerlessFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "LayerOneArn" }] + } + }, + "ChangedLayerVersionServerlessFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "ChangedLayerArn" }] + } + }, + "ReferenceServerlessLayerVersionServerlessFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "MyCustomServerlessLayer" }] + } + }, + "ReferenceLambdaLayerVersionServerlessFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "MyCustomLambdaLayer" }] + } + }, + "TwoLayerVersionServerlessFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "LayerOneArn" }, { "Ref": "LayerTwoArn" }] + } + }, + "OneLayerVersionLambdaFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "LayerOneArn" }] + } + }, + "ChangedLayerVersionLambdaFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "ChangedLayerArn" }] + } + }, + "ReferenceServerlessLayerVersionLambdaFunction": + { + "Type": "AWS::Lambda::Function", + "Properties": + { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "MyCustomServerlessLayer" }] + } + }, + "ReferenceLambdaLayerVersionLambdaFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "MyCustomLambdaLayer" }] + } + }, + "TwoLayerVersionLambdaFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "LayerOneArn" }, { "Ref": "LayerTwoArn" }] + } + }, + "LayerVersionDoesNotExistFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{ "Ref": "NonExistentLayerArn" }] + } + }, + "LayerVersionAccountDoesNotExistFunction": + { + "Type": "AWS::Serverless::Function", + "Properties": + { + "Handler": "layer-main.handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": + [ + "arn:aws:lambda:us-west-2:111111111101:layer:layerDoesNotExist:1" + ] + } + }, + "MyCustomLambdaLayer": + { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { "Content": "custom_layer/" } + }, + "MyCustomServerlessLayer": + { + "Type": "AWS::Serverless::LayerVersion", + "Properties": { "ContentUri": "custom_layer/" } + } + } +} diff --git a/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_methods_resource_resolution.json b/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_methods_resource_resolution.json new file mode 100644 index 0000000000..ea6d0e84cf --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_data/inputs/test_methods_resource_resolution.json @@ -0,0 +1,449 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "HelloHandlerServiceRole11EF7C63": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + "iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ServiceRole/Resource" + } + }, + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": ".", + "Handler": "main.handler", + "Runtime": "python3.6" + } + }, + "HelloHandlerApiPermissionANYAC4E141E": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HelloHandler2E4FBA4D" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "EndpointEEF1FD8F" + }, + "/", + { + "Ref": "EndpointDeploymentStageprodB78BEEA0" + }, + "/*/" + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ApiPermission.ANY.." + } + }, + "HelloHandlerApiPermissionTestANYDDD56D72": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HelloHandler2E4FBA4D" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "EndpointEEF1FD8F" + }, + "/test-invoke-stage/*/" + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ApiPermission.Test.ANY.." + } + }, + "HelloHandlerApiPermissionANYproxy90E90CD6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HelloHandler2E4FBA4D" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "EndpointEEF1FD8F" + }, + "/", + { + "Ref": "EndpointDeploymentStageprodB78BEEA0" + }, + "/*/{proxy+}" + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ApiPermission.ANY..{proxy+}" + } + }, + "HelloHandlerApiPermissionTestANYproxy9803526C": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HelloHandler2E4FBA4D" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "EndpointEEF1FD8F" + }, + "/test-invoke-stage/*/{proxy+}" + ] + ] + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ApiPermission.Test.ANY..{proxy+}" + } + }, + "EndpointEEF1FD8F": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "Endpoint" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/Resource" + } + }, + "EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "Endpointproxy39E2174E", + "EndpointANY485C938B", + "EndpointproxyANYC09721C5" + ], + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/Deployment/Resource" + } + }, + "EndpointDeploymentStageprodB78BEEA0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "DeploymentId": { + "Ref": "EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf" + }, + "StageName": "prod" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/DeploymentStage.prod/Resource" + } + }, + "EndpointCloudWatchRoleC3C64E0F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + "iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/CloudWatchRole/Resource" + } + }, + "EndpointAccountB8304247": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "EndpointCloudWatchRoleC3C64E0F", + "Arn" + ] + } + }, + "DependsOn": [ + "EndpointEEF1FD8F" + ], + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/Account" + } + }, + "Endpointproxy39E2174E": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": { + "Fn::GetAtt": [ + "EndpointEEF1FD8F", + "RootResourceId" + ] + }, + "PathPart": "{proxy+}", + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/{proxy+}/Resource" + } + }, + "EndpointproxyANYC09721C5": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Ref": "Endpointproxy39E2174E" + }, + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + "lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/{proxy+}/ANY/Resource" + } + }, + "EndpointANY485C938B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "EndpointEEF1FD8F", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "EndpointEEF1FD8F" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + "lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/ANY/Resource" + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Modules": "aws-cdk=0.22.0,jsii-runtime=node.js/v12.4.0" + } + } + }, + "Parameters": { + "HelloHandlerCodeS3Bucket4359A483": { + "Type": "String", + "Description": "S3 bucket for asset \"CdkWorkshopStack/HelloHandler/Code\"" + }, + "HelloHandlerCodeS3VersionKey07D12610": { + "Type": "String", + "Description": "S3 key for asset version \"CdkWorkshopStack/HelloHandler/Code\"" + } + }, + "Outputs": { + "Endpoint8024A810": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "EndpointEEF1FD8F" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "EndpointDeploymentStageprodB78BEEA0" + }, + "/" + ] + ] + }, + "Export": { + "Name": "CdkWorkshopStack:Endpoint8024A810" + } + } + } +} \ No newline at end of file diff --git a/tests/unit/lib/intrinsic_resolver/test_data/outputs/output_test_intrinsic_template_resolution.json b/tests/unit/lib/intrinsic_resolver/test_data/outputs/output_test_intrinsic_template_resolution.json new file mode 100644 index 0000000000..ce04aaea73 --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_data/outputs/output_test_intrinsic_template_resolution.json @@ -0,0 +1,101 @@ +{ + "Mappings": { + "TopLevel": { + "SecondLevelKey": { + "key": "https://s3location/" + } + } + }, + "Conditions": { + "ComplexCondition": { + "Fn::And": [ + { + "Fn::Equals": [ + { + "Fn::Or": [ + { + "Condition": "NotTestCondition" + }, + { + "Condition": "TestCondition" + } + ] + }, + false + ] + }, + true, + { + "Fn::If": [ + "TestCondition", + true, + false + ] + } + ] + }, + "TestCondition": { + "Fn::Equals": [ + { + "Ref": "EnvironmentType" + }, + "prod" + ] + }, + "NotTestCondition": { + "Fn::Not": [ + { + "Condition": "TestCondition" + } + ] + }, + "InvalidCondition": [ + "random items" + ] + }, + "Resources": { + "ReferenceLambdaLayerVersionLambdaFunction": { + "Properties": { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + {"Ref": "MyCustomLambdaLayer"} + ] + }, + "Type": "AWS::Serverless::Function" + }, + "MyCustomLambdaLayer": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": "custom_layer/" + } + }, + "RestApi.Deployment": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": "YTtlO2Y7ZA==", + "BodyS3Location": "https://s3location/" + } + }, + "RestApiResource": { + "Properties": { + "parentId": "/", + "PathPart": "{proxy+}", + "RestApiId": "RestApi.Deployment" + } + }, + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "handler": "main.handle" + } + }, + "LambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Uri": "arn:aws:apigateway:us-east-1a:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:HelloHandler2E4FBA4D/invocations" + } + } + } +} \ No newline at end of file diff --git a/tests/unit/lib/intrinsic_resolver/test_data/outputs/outputs_methods_resource_resolution.json b/tests/unit/lib/intrinsic_resolver/test_data/outputs/outputs_methods_resource_resolution.json new file mode 100644 index 0000000000..1b0cccd711 --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_data/outputs/outputs_methods_resource_resolution.json @@ -0,0 +1,223 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "HelloHandlerServiceRole11EF7C63": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:awsiam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ServiceRole/Resource" + } + }, + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": ".", + "Handler": "main.handler", + "Runtime": "python3.6" + } + }, + "HelloHandlerApiPermissionANYAC4E141E": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": "HelloHandler2E4FBA4D", + "Principal": "apigateway.amazonaws.com", + "SourceArn": "arn:aws:execute-api:us-east-1:123456789012:EndpointEEF1FD8F/EndpointDeploymentStageprodB78BEEA0/*/" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ApiPermission.ANY.." + } + }, + "HelloHandlerApiPermissionTestANYDDD56D72": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": "HelloHandler2E4FBA4D", + "Principal": "apigateway.amazonaws.com", + "SourceArn": "arn:aws:execute-api:us-east-1:123456789012:EndpointEEF1FD8F/test-invoke-stage/*/" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ApiPermission.Test.ANY.." + } + }, + "HelloHandlerApiPermissionANYproxy90E90CD6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": "HelloHandler2E4FBA4D", + "Principal": "apigateway.amazonaws.com", + "SourceArn": "arn:aws:execute-api:us-east-1:123456789012:EndpointEEF1FD8F/EndpointDeploymentStageprodB78BEEA0/*/{proxy+}" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ApiPermission.ANY..{proxy+}" + } + }, + "HelloHandlerApiPermissionTestANYproxy9803526C": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": "HelloHandler2E4FBA4D", + "Principal": "apigateway.amazonaws.com", + "SourceArn": "arn:aws:execute-api:us-east-1:123456789012:EndpointEEF1FD8F/test-invoke-stage/*/{proxy+}" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/HelloHandler/ApiPermission.Test.ANY..{proxy+}" + } + }, + "EndpointEEF1FD8F": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "Endpoint" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/Resource" + } + }, + "EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": "EndpointEEF1FD8F", + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "Endpointproxy39E2174E", + "EndpointANY485C938B", + "EndpointproxyANYC09721C5" + ], + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/Deployment/Resource" + } + }, + "EndpointDeploymentStageprodB78BEEA0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": "EndpointEEF1FD8F", + "DeploymentId": "EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf", + "StageName": "prod" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/DeploymentStage.prod/Resource" + } + }, + "EndpointCloudWatchRoleC3C64E0F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:awsiam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/CloudWatchRole/Resource" + } + }, + "EndpointAccountB8304247": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": "arn:aws:lambda:us-east-1:123456789012:function:EndpointCloudWatchRoleC3C64E0F" + }, + "DependsOn": [ + "EndpointEEF1FD8F" + ], + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/Account" + } + }, + "Endpointproxy39E2174E": { + "Type": "AWS::ApiGateway::Resource", + "Properties": { + "ParentId": "/", + "PathPart": "{proxy+}", + "RestApiId": "EndpointEEF1FD8F" + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/{proxy+}/Resource" + } + }, + "EndpointproxyANYC09721C5": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": "Endpointproxy39E2174E", + "RestApiId": "EndpointEEF1FD8F", + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": "arn:aws:apigateway:us-east-1lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:HelloHandler2E4FBA4D/invocations" + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/{proxy+}/ANY/Resource" + } + }, + "EndpointANY485C938B": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": "/", + "RestApiId": "EndpointEEF1FD8F", + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": "arn:aws:apigateway:us-east-1lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:HelloHandler2E4FBA4D/invocations" + } + }, + "Metadata": { + "aws:cdk:path": "CdkWorkshopStack/Endpoint/ANY/Resource" + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Modules": "aws-cdk=0.22.0,jsii-runtime=node.js/v12.4.0" + } + } + }, + "Parameters": { + "HelloHandlerCodeS3Bucket4359A483": { + "Type": "String", + "Description": "S3 bucket for asset \"CdkWorkshopStack/HelloHandler/Code\"" + }, + "HelloHandlerCodeS3VersionKey07D12610": { + "Type": "String", + "Description": "S3 key for asset version \"CdkWorkshopStack/HelloHandler/Code\"" + } + }, + "Outputs": { + "Endpoint8024A810": { + "Value": "https://EndpointEEF1FD8F.execute-api.us-east-1.amazonaws.com/EndpointDeploymentStageprodB78BEEA0/", + "Export": { + "Name": "CdkWorkshopStack:Endpoint8024A810" + } + } + } +} \ No newline at end of file diff --git a/tests/unit/lib/intrinsic_resolver/test_data/outputs/outputs_test_layers_resolution.json b/tests/unit/lib/intrinsic_resolver/test_data/outputs/outputs_test_layers_resolution.json new file mode 100644 index 0000000000..0db313fd7e --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_data/outputs/outputs_test_layers_resolution.json @@ -0,0 +1,169 @@ +{ + "Transform": "AWS::Serverless-2016-10-31", + "Parameters": { + "LayerOneArn": { + "Default": "arn:aws:lambda:us-west-2:111111111111:layer:layer:1", + "Type": "String" + }, + "LayerTwoArn": { + "Default": "arn:aws:lambda:us-west-2:111111111111:layer:layer2:1", + "Type": "String" + }, + "ChangedLayerArn": { + "Default": "arn:aws:lambda:us-west-2:111111111111:layer:changed_layer:1", + "Type": "String" + }, + "NonExistentLayerArn": { + "Default": "arn:aws:lambda:us-west-2:111111111111:layer:non_existent_layer:1", + "Type": "String" + } + }, + "Resources": { + "OneLayerVersionServerlessFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + "arn:aws:lambda:us-west-2:111111111111:layer:layer:1" + ] + } + }, + "ChangedLayerVersionServerlessFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + "arn:aws:lambda:us-west-2:111111111111:layer:changed_layer:1" + ] + } + }, + "ReferenceServerlessLayerVersionServerlessFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + {"Ref": "MyCustomServerlessLayer"} + ] + } + }, + "ReferenceLambdaLayerVersionServerlessFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + {"Ref": "MyCustomLambdaLayer"} + ] + } + }, + "TwoLayerVersionServerlessFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + "arn:aws:lambda:us-west-2:111111111111:layer:layer:1", + "arn:aws:lambda:us-west-2:111111111111:layer:layer2:1" + ] + } + }, + "OneLayerVersionLambdaFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + "arn:aws:lambda:us-west-2:111111111111:layer:layer:1" + ] + } + }, + "ChangedLayerVersionLambdaFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + "arn:aws:lambda:us-west-2:111111111111:layer:changed_layer:1" + ] + } + }, + "ReferenceServerlessLayerVersionLambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + {"Ref": "MyCustomServerlessLayer"} + ] + } + }, + "ReferenceLambdaLayerVersionLambdaFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + {"Ref": "MyCustomLambdaLayer"} + ] + } + }, + "TwoLayerVersionLambdaFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.one_layer_hanlder", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + "arn:aws:lambda:us-west-2:111111111111:layer:layer:1", + "arn:aws:lambda:us-west-2:111111111111:layer:layer2:1" + ] + } + }, + "LayerVersionDoesNotExistFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + "arn:aws:lambda:us-west-2:111111111111:layer:non_existent_layer:1" + ] + } + }, + "LayerVersionAccountDoesNotExistFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "layer-main.handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [ + "arn:aws:lambda:us-west-2:111111111101:layer:layerDoesNotExist:1" + ] + } + }, + "MyCustomLambdaLayer": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": "custom_layer/" + } + }, + "MyCustomServerlessLayer": { + "Type": "AWS::Serverless::LayerVersion", + "Properties": { + "ContentUri": "custom_layer/" + } + } + } +} \ No newline at end of file diff --git a/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py new file mode 100644 index 0000000000..b257604ab1 --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_intrinsic_resolver.py @@ -0,0 +1,1550 @@ +import json +from collections import OrderedDict +from copy import deepcopy + +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path +from unittest import TestCase + +from parameterized import parameterized + +from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver +from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable +from samcli.lib.intrinsic_resolver.invalid_intrinsic_exception import ( + InvalidIntrinsicException, +) + + +class TestIntrinsicFnJoinResolver(TestCase): + def setUp(self): + self.resolver = IntrinsicResolver(template={}, symbol_resolver=IntrinsicsSymbolTable()) + + def test_basic_fn_join(self): + intrinsic = {"Fn::Join": [",", ["a", "b", "c", "d"]]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "a,b,c,d") + + def test_nested_fn_join(self): + intrinsic_base_1 = {"Fn::Join": [",", ["a", "b", "c", "d"]]} + intrinsic_base_2 = {"Fn::Join": [";", ["g", "h", "i", intrinsic_base_1]]} + intrinsic = {"Fn::Join": [":", [intrinsic_base_1, "e", "f", intrinsic_base_2]]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "a,b,c,d:e:f:g;h;i;a,b,c,d") + + @parameterized.expand( + [ + ( + "Fn::Join should fail for values that are not lists: {}".format(item), + item, + ) + for item in [True, False, "Test", {}, 42, None] + ] + ) + def test_fn_join_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Join": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn::Join should fail if the first argument does not resolve to a string: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42, None] + ] + ) + def test_fn_join_delimiter_invalid_type(self, name, delimiter): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Join": [delimiter, []]}) + + @parameterized.expand( + [ + ( + "Fn::Join should fail if the list_of_objects is not a valid list: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42, "t", None] + ] + ) + def test_fn_list_of_objects_invalid_type(self, name, list_of_objects): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::Join": ["", list_of_objects]} + ) + + @parameterized.expand( + [ + ( + "Fn::Join should require that all items in the list_of_objects resolve to string: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42, None] + ] + ) + def test_fn_join_items_all_str(self, name, single_obj): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::Join": ["", ["test", single_obj, "abcd"]]} + ) + + +class TestIntrinsicFnSplitResolver(TestCase): + def setUp(self): + self.resolver = IntrinsicResolver(template={}, symbol_resolver=IntrinsicsSymbolTable()) + + def test_basic_fn_split(self): + intrinsic = {"Fn::Split": ["|", "a|b|c"]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, ["a", "b", "c"]) + + def test_nested_fn_split(self): + intrinsic_base_1 = {"Fn::Split": [";", {"Fn::Join": [";", ["a", "b", "c"]]}]} + + intrinsic_base_2 = {"Fn::Join": [",", intrinsic_base_1]} + intrinsic = { + "Fn::Split": [ + ",", + {"Fn::Join": [",", [intrinsic_base_2, ",e", ",f,", intrinsic_base_2]]}, + ] + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, ["a", "b", "c", "", "e", "", "f", "", "a", "b", "c"]) + + @parameterized.expand( + [ + ( + "Fn::Split should fail for values that are not lists: {}".format(item), + item, + ) + for item in [True, False, "Test", {}, 42] + ] + ) + def test_fn_split_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Split": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn::Split should fail if the first argument does not resolve to a string: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42] + ] + ) + def test_fn_split_delimiter_invalid_type(self, name, delimiter): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Split": [delimiter, []]}) + + @parameterized.expand( + [ + ( + "Fn::Split should fail if the second argument does not resolve to a string: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42] + ] + ) + def test_fn_split_source_string_invalid_type(self, name, source_string): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::Split": ["", source_string]} + ) + + +class TestIntrinsicFnBase64Resolver(TestCase): + def setUp(self): + self.resolver = IntrinsicResolver(template={}, symbol_resolver=IntrinsicsSymbolTable()) + + def test_basic_fn_split(self): + intrinsic = {"Fn::Base64": "AWS CloudFormation"} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "QVdTIENsb3VkRm9ybWF0aW9u") + + def test_nested_fn_base64(self): + intrinsic_base_1 = {"Fn::Base64": "AWS CloudFormation"} + + intrinsic_base_2 = {"Fn::Base64": intrinsic_base_1} + intrinsic = { + "Fn::Base64": { + "Fn::Join": [",", [intrinsic_base_2, ",e", ",f,", intrinsic_base_2]] + } + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual( + result, + "VVZaa1ZFbEZUbk5pTTFaclVtMDVlV0pYUmpCaFZ6bDEsLGUsLGYsLFVWWmtWRWxGVG5OaU0xWnJ" + "VbTA1ZVdKWFJqQmhWemwx", + ) + + @parameterized.expand( + [ + ( + "Fn::Base64 must have a value that resolves to a string: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42, None] + ] + ) + def test_fn_base64_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Base64": intrinsic}) + + +class TestIntrinsicFnSelectResolver(TestCase): + def setUp(self): + self.resolver = IntrinsicResolver(template={}, symbol_resolver=IntrinsicsSymbolTable()) + + def test_basic_fn_select(self): + intrinsic = {"Fn::Select": [2, ["a", "b", "c", "d"]]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "c") + + def test_nested_fn_select(self): + intrinsic_base_1 = {"Fn::Select": [0, ["a", "b", "c", "d"]]} + intrinsic_base_2 = {"Fn::Join": [";", ["g", "h", "i", intrinsic_base_1]]} + intrinsic = {"Fn::Select": [3, [intrinsic_base_2, "e", "f", intrinsic_base_2]]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "g;h;i;a") + + @parameterized.expand( + [ + ( + "Fn::Select should fail for values that are not lists: {}".format(item), + item, + ) + for item in [True, False, "Test", {}, 42, None] + ] + ) + def test_fn_select_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Select": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn::Select should fail if the first argument does not resolve to a int: {}".format( + item + ), + item, + ) + for item in [True, False, {}, "3", None] + ] + ) + def test_fn_select_index_invalid_index_type(self, name, index): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Select": [index, [0]]}) + + @parameterized.expand( + [ + ( + "Fn::Select should fail if the index is out of bounds: {}".format( + number + ), + number, + ) + for number in [-2, 7] + ] + ) + def test_fn_select_out_of_bounds(self, name, index): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Select": [index, []]}) + + @parameterized.expand( + [ + ( + "Fn::Select should fail if the second argument does not resolve to a list: {}".format( + item + ), + item, + ) + for item in [True, False, {}, "3", 33, None] + ] + ) + def test_fn_select_second_argument_invalid_type(self, name, argument): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Select": [0, argument]}) + + +class TestIntrinsicFnFindInMapResolver(TestCase): + def setUp(self): + template = { + "Mappings": { + "Basic": {"Test": {"key": "value"}}, + "value": {"anotherkey": {"key": "result"}}, + "result": {"value": {"key": "final"}}, + } + } + self.resolver = IntrinsicResolver( + symbol_resolver=IntrinsicsSymbolTable(), template=template + ) + + def test_basic_find_in_map(self): + intrinsic = {"Fn::FindInMap": ["Basic", "Test", "key"]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "value") + + def test_nested_find_in_map(self): + intrinsic_base_1 = {"Fn::FindInMap": ["Basic", "Test", "key"]} + intrinsic_base_2 = {"Fn::FindInMap": [intrinsic_base_1, "anotherkey", "key"]} + intrinsic = {"Fn::FindInMap": [intrinsic_base_2, intrinsic_base_1, "key"]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "final") + + @parameterized.expand( + [ + ( + "Fn::FindInMap should fail if the list does not resolve to a string: {}".format( + item + ), + item, + ) + for item in [True, False, "Test", {}, 42, None] + ] + ) + def test_fn_find_in_map_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::FindInMap": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn::FindInMap should fail if there isn't 3 arguments in the list: {}".format( + item + ), + item, + ) + for item in [[""] * i for i in [0, 1, 2, 4, 5, 6, 7, 8, 9, 10]] + ] + ) + def test_fn_find_in_map_invalid_number_arguments(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::FindInMap": intrinsic}) + + @parameterized.expand( + [ + ( + "The arguments in Fn::FindInMap must fail if the arguments are not in the mappings".format( + item + ), + item, + ) + for item in [ + ["", "Test", "key"], + ["Basic", "", "key"], + ["Basic", "Test", ""], + ] + ] + ) + def test_fn_find_in_map_invalid_key_entries(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::FindInMap": intrinsic}) + + +class TestIntrinsicFnAzsResolver(TestCase): + def setUp(self): + logical_id_translator = {"AWS::Region": "us-east-1"} + self.resolver = IntrinsicResolver( + template={}, + symbol_resolver=IntrinsicsSymbolTable( + logical_id_translator=logical_id_translator + ) + ) + + def test_basic_azs(self): + intrinsic = {"Ref": "AWS::Region"} + result = self.resolver.intrinsic_property_resolver({"Fn::GetAZs": intrinsic}) + self.assertEqual( + result, + [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f", + ], + ) + + def test_default_get_azs(self): + result = self.resolver.intrinsic_property_resolver({"Fn::GetAZs": ""}) + self.assertEqual( + result, + [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f", + ], + ) + + @parameterized.expand( + [ + ("Fn::GetAZs should fail if it not given a string type".format(item), item) + for item in [True, False, {}, 42, None] + ] + ) + def test_fn_azs_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::GetAZs": intrinsic}) + + def test_fn_azs_invalid_region(self): + intrinsic = "UNKOWN REGION" + with self.assertRaises(InvalidIntrinsicException, msg="FN::GetAzs should fail for unknown region"): + self.resolver.intrinsic_property_resolver({"Fn::GetAZs": intrinsic}) + + +class TestFnTransform(TestCase): + def setUp(self): + logical_id_translator = {"AWS::Region": "us-east-1"} + self.resolver = IntrinsicResolver( + template={}, + symbol_resolver=IntrinsicsSymbolTable( + logical_id_translator=logical_id_translator + ) + ) + + def test_basic_fn_transform(self): + intrinsic = {"Fn::Transform": {"Name": "AWS::Include", "Parameters": {"Location": "test"}}} + self.resolver.intrinsic_property_resolver(intrinsic) + + def test_fn_transform_unsupported_macro(self): + intrinsic = {"Fn::Transform": {"Name": "UNKNOWN", "Parameters": {"Location": "test"}}} + with self.assertRaises(InvalidIntrinsicException, msg="FN::Transform should fail for unknown region"): + self.resolver.intrinsic_property_resolver(intrinsic) + + +class TestIntrinsicFnRefResolver(TestCase): + def setUp(self): + logical_id_translator = { + "RestApi": {"Ref": "NewRestApi"}, + "AWS::StackId": "12301230123", + } + resources = {"RestApi": {"Type": "AWS::ApiGateway::RestApi", "Properties": {}}} + template = {"Resources": resources} + self.resolver = IntrinsicResolver( + symbol_resolver=IntrinsicsSymbolTable( + logical_id_translator=logical_id_translator, template=template + ), template=template + ) + + def test_basic_ref_translation(self): + intrinsic = {"Ref": "RestApi"} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "NewRestApi") + + def test_default_ref_translation(self): + intrinsic = {"Ref": "UnknownApi"} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "UnknownApi") + + @parameterized.expand( + [ + ("Ref must have arguments that resolve to a string: {}".format(item), item) + for item in [True, False, {}, 42, None, []] + ] + ) + def test_ref_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Ref": intrinsic}) + + +class TestIntrinsicFnGetAttResolver(TestCase): + def setUp(self): + logical_id_translator = { + "RestApi": {"Ref": "NewRestApi"}, + "LambdaFunction": { + "Arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east" + "-1:123456789012:LambdaFunction/invocations" + }, + "AWS::StackId": "12301230123", + "AWS::Region": "us-east-1", + "AWS::AccountId": "406033500479", + } + resources = { + "RestApi": {"Type": "AWS::ApiGateway::RestApi", "Properties": {}}, + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": {"handler": "main.handle"}, + }, + "LambdaFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + {"Ref": "AWS::Partition"}, + ":apigateway:", + {"Ref": "AWS::Region"}, + ":lambda:path/2015-03-31/functions/", + {"Fn::GetAtt": ["HelloHandler2E4FBA4D", "Arn"]}, + "/invocations", + ], + ] + } + }, + }, + } + template = {"Resources": resources} + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator=logical_id_translator + ) + self.resources = resources + self.resolver = IntrinsicResolver( + template=template, symbol_resolver=symbol_resolver + ) + + def test_fn_getatt_basic_translation(self): + intrinsic = {"Fn::GetAtt": ["RestApi", "RootResourceId"]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual(result, "/") + + def test_fn_getatt_logical_id_translated(self): + intrinsic = {"Fn::GetAtt": ["LambdaFunction", "Arn"]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual( + result, + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east" + "-1:123456789012:LambdaFunction/invocations", + ) + + def test_fn_getatt_with_fn_join(self): + intrinsic = self.resources.get("LambdaFunction").get("Properties").get("Uri") + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual( + result, + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us" + "-east-1:406033500479:function:HelloHandler2E4FBA4D/invocations", + ) + + @parameterized.expand( + [ + ( + "Fn::GetAtt must fail if the argument does not resolve to a list: {}".format( + item + ), + item, + ) + for item in [True, False, {}, "test", 42, None] + ] + ) + def test_fn_getatt_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::GetAtt": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn::GetAtt should fail if it doesn't have exactly 2 arguments: {}".format( + item + ), + item, + ) + for item in [[""] * i for i in [0, 1, 3, 4, 5, 6, 7, 8, 9, 10]] + ] + ) + def test_fn_getatt_invalid_number_arguments(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::GetAtt": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn::GetAtt first argument must resolve to a valid string: {}".format( + item + ), + item, + ) + for item in [True, False, {}, [], 42, None] + ] + ) + def test_fn_getatt_first_arguments_invalid(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::GetAtt": [intrinsic, IntrinsicResolver.REF]} + ) + + @parameterized.expand( + [ + ( + "Fn::GetAtt second argument must resolve to a string:{}".format(item), + item, + ) + for item in [True, False, {}, [], 42, None] + ] + ) + def test_fn_getatt_second_arguments_invalid(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::GetAtt": ["some logical Id", intrinsic]} + ) + + +class TestIntrinsicFnSubResolver(TestCase): + def setUp(self): + logical_id_translator = { + "AWS::Region": "us-east-1", + "AWS::AccountId": "123456789012", + } + resources = { + "LambdaFunction": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": {"Uri": "test"}, + } + } + template = {"Resources": resources} + self.resolver = IntrinsicResolver( + template=template, + symbol_resolver=IntrinsicsSymbolTable( + logical_id_translator=logical_id_translator, template=template + ) + ) + + def test_fn_sub_basic_uri(self): + intrinsic = { + "Fn::Sub": + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations" + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual( + result, + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1" + ":123456789012:function:LambdaFunction/invocations", + ) + + def test_fn_sub_uri_arguments(self): + intrinsic = { + "Fn::Sub": [ + "arn:aws:apigateway:${MyItem}:lambda:path/2015-03-31/functions/${MyOtherItem}/invocations", + {"MyItem": {"Ref": "AWS::Region"}, "MyOtherItem": "LambdaFunction.Arn"}, + ] + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertEqual( + result, + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east" + "-1:123456789012:function:LambdaFunction/invocations", + ) + + @parameterized.expand( + [ + ( + "Fn::Sub arguments must either resolve to a string or a list".format( + item + ), + item, + ) + for item in [True, False, {}, 42, None] + ] + ) + def test_fn_sub_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Sub": intrinsic}) + + @parameterized.expand( + [ + ( + "If Fn::Sub is a list, first argument must resolve to a string: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42, None] + ] + ) + def test_fn_sub_first_argument_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Sub": [intrinsic, {}]}) + + @parameterized.expand( + [ + ( + "If Fn::Sub is a list, second argument must resolve to a dictionary".format( + item + ), + item, + ) + for item in [True, False, "Another str", [], 42, None] + ] + ) + def test_fn_sub_second_argument_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::Sub": ["some str", intrinsic]} + ) + + @parameterized.expand( + [ + ("If Fn::Sub is a list, it should only have 2 arguments".format(item), item) + for item in [[""] * i for i in [0, 2, 3, 4, 5, 6, 7, 8, 9, 10]] + ] + ) + def test_fn_sub_invalid_number_arguments(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Sub": ["test"] + intrinsic}) + + +class TestIntrinsicFnImportValueResolver(TestCase): + def setUp(self): + self.resolver = IntrinsicResolver(template={}, symbol_resolver=IntrinsicsSymbolTable()) + + def test_fn_import_value_unsupported(self): + with self.assertRaises( + InvalidIntrinsicException, msg="Fn::ImportValue should be unsupported" + ): + self.resolver.intrinsic_property_resolver({"Fn::ImportValue": ""}) + + +class TestIntrinsicFnEqualsResolver(TestCase): + def setUp(self): + logical_id_translator = { + "EnvironmentType": "prod", + "AWS::AccountId": "123456789012", + } + self.resolver = IntrinsicResolver( + template={}, + symbol_resolver=IntrinsicsSymbolTable( + logical_id_translator=logical_id_translator + ) + ) + + def test_fn_equals_basic_true(self): + intrinsic = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_equals_basic_false(self): + intrinsic = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "NotProd"]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + def test_fn_equals_nested_true(self): + intrinsic_base_1 = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + intrinsic_base_2 = {"Fn::Equals": [{"Ref": "AWS::AccountId"}, "123456789012"]} + + intrinsic = {"Fn::Equals": [intrinsic_base_1, intrinsic_base_2]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_equals_nested_false(self): + intrinsic_base_1 = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + intrinsic_base_2 = { + "Fn::Equals": [{"Ref": "AWS::AccountId"}, "NOT_A_VALID_ACCOUNT_ID"] + } + + intrinsic = {"Fn::Equals": [intrinsic_base_1, intrinsic_base_2]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + @parameterized.expand( + [ + ( + "Fn::Equals must have arguments that resolve to a string: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42, None, "test"] + ] + ) + def test_fn_equals_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Equals": intrinsic}) + + @parameterized.expand( + [ + ("Fn::Equals must have exactly two arguments: {}".format(item), item) + for item in [["t"] * i for i in [0, 1, 3, 4, 5, 6, 7, 8, 9, 10]] + ] + ) + def test_fn_equals_invalid_number_arguments(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Equals": intrinsic}) + + +class TestIntrinsicFnNotResolver(TestCase): + def setUp(self): + logical_id_translator = { + "EnvironmentType": "prod", + "AWS::AccountId": "123456789012", + } + conditions = { + "TestCondition": {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]}, + "NotTestCondition": {"Fn::Not": [{"Condition": "TestCondition"}]}, + } + template = {"Conditions": conditions} + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator=logical_id_translator + ) + self.resolver = IntrinsicResolver( + template=template, symbol_resolver=symbol_resolver + ) + + def test_fn_not_basic_false(self): + intrinsic = {"Fn::Not": [{"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]}]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + def test_fn_not_basic_true(self): + intrinsic = { + "Fn::Not": [{"Fn::Equals": [{"Ref": "EnvironmentType"}, "NotProd"]}] + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_not_nested_true(self): + intrinsic_base_1 = { + "Fn::Not": [{"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]}] + } + intrinsic_base_2 = {"Fn::Equals": [{"Ref": "AWS::AccountId"}, "123456789012"]} + # !(True && True) + intrinsic = {"Fn::Not": [{"Fn::Equals": [intrinsic_base_1, intrinsic_base_2]}]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_not_nested_false(self): + intrinsic_base_1 = { + "Fn::Not": [{"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]}] + } + intrinsic_base_2 = { + "Fn::Not": [{"Fn::Equals": [{"Ref": "AWS::AccountId"}, "123456789012"]}] + } + + intrinsic = {"Fn::Not": [{"Fn::Equals": [intrinsic_base_1, intrinsic_base_2]}]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + def test_fn_not_condition_false(self): + intrinsic = {"Fn::Not": [{"Condition": "TestCondition"}]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + def test_fn_not_condition_true(self): + intrinsic = {"Fn::Not": [{"Condition": "NotTestCondition"}]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + @parameterized.expand( + [ + ( + "Fn::Not must have an argument that resolves to a list: {}".format( + item + ), + item, + ) + for item in [True, False, {}, 42, None, "test"] + ] + ) + def test_fn_not_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Not": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn::Not items in the list must resolve to booleans: {}".format(item), + item, + ) + for item in [{}, 42, None, "test"] + ] + ) + def test_fn_not_first_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Not": [intrinsic]}) + + @parameterized.expand( + [ + ("Fn::Not must have exactly 1 argument: {}".format(item), item) + for item in [[True] * i for i in [0, 2, 3, 4, 5, 6, 7, 8, 9, 10]] + ] + ) + def test_fn_not_invalid_number_arguments(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Not": intrinsic}) + + def test_fn_not_invalid_condition(self): + with self.assertRaises(InvalidIntrinsicException, msg="Invalid Condition"): + self.resolver.intrinsic_property_resolver( + {"Fn::Not": [{"Condition": "NOT_VALID_CONDITION"}]} + ) + + +class TestIntrinsicFnAndResolver(TestCase): + def setUp(self): + logical_id_translator = { + "EnvironmentType": "prod", + "AWS::AccountId": "123456789012", + } + conditions = { + "TestCondition": {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]}, + "NotTestCondition": {"Fn::Not": [{"Condition": "TestCondition"}]}, + } + template = {"Conditions": conditions} + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator=logical_id_translator + ) + self.resolver = IntrinsicResolver( + template=template, symbol_resolver=symbol_resolver + ) + + def test_fn_and_basic_true(self): + prod_fn_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + intrinsic = { + "Fn::And": [prod_fn_equals, {"Condition": "TestCondition"}, prod_fn_equals] + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_and_basic_false(self): + prod_fn_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + intrinsic = { + "Fn::And": [ + prod_fn_equals, + {"Condition": "NotTestCondition"}, + prod_fn_equals, + ] + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + def test_fn_and_nested_true(self): + prod_fn_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + intrinsic_base = { + "Fn::And": [prod_fn_equals, {"Condition": "TestCondition"}, prod_fn_equals] + } + fn_not_intrinsic = {"Fn::Not": [{"Condition": "NotTestCondition"}]} + intrinsic = {"Fn::And": [intrinsic_base, fn_not_intrinsic, prod_fn_equals]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_and_nested_false(self): + prod_fn_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + prod_fn_not_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "NOT_EQUAL"]} + intrinsic_base = { + "Fn::And": [ + prod_fn_equals, + {"Condition": "NotTestCondition"}, + prod_fn_equals, + ] + } + intrinsic = {"Fn::And": [{"Fn::Not": [intrinsic_base]}, prod_fn_not_equals]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + @parameterized.expand( + [ + ("Fn::And must have value that resolves to a list: {}".format(item), item) + for item in [True, False, {}, 42, None, "test"] + ] + ) + def test_fn_and_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::And": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn:And must have all arguments that resolves to booleans".format(item), + item, + ) + for item in [{}, 42, None, "test"] + ] + ) + def test_fn_and_all_arguments_bool(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::And": [intrinsic, intrinsic, intrinsic]} + ) + + def test_fn_and_invalid_condition(self): + with self.assertRaises(InvalidIntrinsicException, msg="Invalid Condition"): + self.resolver.intrinsic_property_resolver( + {"Fn::And": [{"Condition": "NOT_VALID_CONDITION"}]} + ) + + +class TestIntrinsicFnOrResolver(TestCase): + def setUp(self): + logical_id_translator = { + "EnvironmentType": "prod", + "AWS::AccountId": "123456789012", + } + conditions = { + "TestCondition": {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]}, + "NotTestCondition": {"Fn::Not": [{"Condition": "TestCondition"}]}, + } + + template = {"Conditions": conditions} + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator=logical_id_translator + ) + self.resolver = IntrinsicResolver( + template=template, symbol_resolver=symbol_resolver + ) + + def test_fn_or_basic_true(self): + prod_fn_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + intrinsic = { + "Fn::Or": [prod_fn_equals, {"Condition": "TestCondition"}, prod_fn_equals] + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_or_basic_single_true(self): + intrinsic = {"Fn::Or": [False, False, {"Condition": "TestCondition"}, False]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_or_basic_false(self): + prod_fn_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + intrinsic = { + "Fn::Or": [ + {"Fn::Not": [prod_fn_equals]}, + {"Condition": "NotTestCondition"}, + {"Fn::Not": [prod_fn_equals]}, + ] + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + def test_fn_or_nested_true(self): + prod_fn_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + failed_intrinsic_or = { + "Fn::Or": [ + {"Fn::Not": [prod_fn_equals]}, + {"Condition": "NotTestCondition"}, + {"Fn::Not": [prod_fn_equals]}, + ] + } + intrinsic_base = { + "Fn::Or": [prod_fn_equals, {"Condition": "TestCondition"}, prod_fn_equals] + } + fn_not_intrinsic = {"Fn::Not": [{"Condition": "NotTestCondition"}]} + intrinsic = { + "Fn::Or": [ + failed_intrinsic_or, + intrinsic_base, + fn_not_intrinsic, + fn_not_intrinsic, + ] + } + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_or_nested_false(self): + prod_fn_equals = {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]} + failed_intrinsic_or = { + "Fn::Or": [ + {"Fn::Not": [prod_fn_equals]}, + {"Condition": "NotTestCondition"}, + {"Fn::Not": [prod_fn_equals]}, + ] + } + intrinsic_base = { + "Fn::Or": [prod_fn_equals, {"Condition": "TestCondition"}, prod_fn_equals] + } + intrinsic = {"Fn::Or": [failed_intrinsic_or, {"Fn::Not": [intrinsic_base]}]} + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + @parameterized.expand( + [ + ( + "Fn::Or must have an argument that resolves to a list: {}".format(item), + item, + ) + for item in [True, False, {}, 42, None, "test"] + ] + ) + def test_fn_or_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::Or": intrinsic}) + + @parameterized.expand( + [ + ( + "Fn::Or must have all arguments resolve to booleans: {}".format(item), + item, + ) + for item in [{}, 42, None, "test"] + ] + ) + def test_fn_or_all_arguments_bool(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::Or": [intrinsic, intrinsic, intrinsic]} + ) + + def test_fn_or_invalid_condition(self): + with self.assertRaises(InvalidIntrinsicException, msg="Invalid Condition"): + self.resolver.intrinsic_property_resolver( + {"Fn::Or": [{"Condition": "NOT_VALID_CONDITION"}]} + ) + + +class TestIntrinsicFnIfResolver(TestCase): + def setUp(self): + logical_id_translator = { + "EnvironmentType": "prod", + "AWS::AccountId": "123456789012", + } + conditions = { + "TestCondition": {"Fn::Equals": [{"Ref": "EnvironmentType"}, "prod"]}, + "NotTestCondition": {"Fn::Not": [{"Condition": "TestCondition"}]}, + "InvalidCondition": ["random items"], + } + template = {"Conditions": conditions} + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator=logical_id_translator + ) + self.resolver = IntrinsicResolver( + template=template, symbol_resolver=symbol_resolver + ) + + def test_fn_if_basic_true(self): + intrinsic = {"Fn::If": ["TestCondition", True, False]} + + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_fn_if_basic_false(self): + intrinsic = {"Fn::If": ["NotTestCondition", True, False]} + + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + def test_nested_fn_if_true(self): + intrinsic_base_1 = {"Fn::If": ["NotTestCondition", True, False]} + intrinsic_base_2 = {"Fn::If": ["TestCondition", True, False]} + intrinsic = {"Fn::If": ["TestCondition", intrinsic_base_2, intrinsic_base_1]} + + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertTrue(result) + + def test_nested_fn_if_false(self): + intrinsic_base_1 = {"Fn::If": ["NotTestCondition", True, False]} + intrinsic_base_2 = {"Fn::If": ["TestCondition", True, False]} + intrinsic = {"Fn::If": ["TestCondition", intrinsic_base_1, intrinsic_base_2]} + + result = self.resolver.intrinsic_property_resolver(intrinsic) + self.assertFalse(result) + + @parameterized.expand( + [ + ("Fn::If must an argument that resolves to a list: {}".format(item), item) + for item in [True, False, {}, 42, None, "test"] + ] + ) + def test_fn_if_arguments_invalid_formats(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver({"Fn::If": intrinsic}) + + @parameterized.expand( + [ + ("Fn::If must have the argument resolve to a string: {}".format(item), item) + for item in [True, False, {}, 42, None, "test", []] + ] + ) + def test_fn_if_condition_arguments_invalid_type(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::If": [intrinsic, True, False]} + ) + + def test_fn_if_invalid_condition(self): + with self.assertRaises(InvalidIntrinsicException, msg="Invalid Condition"): + self.resolver.intrinsic_property_resolver( + {"Fn::If": ["NOT_VALID_CONDITION", "test", "test"]} + ) + + @parameterized.expand( + [ + ("Fn::If must have exactly 3 arguments: {}".format(item), item) + for item in [[True] * i for i in [0, 1, 3, 4, 5, 6, 7, 8, 9, 10]] + ] + ) + def test_fn_if_invalid_number_arguments(self, name, intrinsic): + with self.assertRaises(InvalidIntrinsicException, msg=name): + self.resolver.intrinsic_property_resolver( + {"Fn::Not": ["TestCondition"] + intrinsic} + ) + + def test_fn_if_condition_not_bool_fail(self): + with self.assertRaises(InvalidIntrinsicException, msg="Invalid Condition"): + self.resolver.intrinsic_property_resolver( + {"Fn::If": ["InvalidCondition", "test", "test"]} + ) + + +class TestIntrinsicAttribteResolution(TestCase): + def setUp(self): + self.maxDiff = None + logical_id_translator = { + "RestApi": "NewRestApi", + "LambdaFunction": { + "Arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east" + "-1:123456789012:LambdaFunction/invocations" + }, + "AWS::StackId": "12301230123", + "AWS::Region": "us-east-1", + "AWS::AccountId": "406033500479", + "RestApi.Deployment": {"Ref": "RestApi"}, + } + self.logical_id_translator = logical_id_translator + + integration_path = str( + Path(__file__).resolve().parents[0].joinpath('test_data', 'inputs/test_intrinsic_template_resolution.json')) + with open(integration_path) as f: + template = json.load(f) + + self.template = template + self.resources = template.get("Resources") + self.conditions = template.get("Conditions") + self.mappings = template.get("Mappings") + + symbol_resolver = IntrinsicsSymbolTable( + template=self.template, logical_id_translator=self.logical_id_translator + ) + self.resolver = IntrinsicResolver( + template=self.template, symbol_resolver=symbol_resolver + ) + + def test_basic_attribte_resolution(self): + resolved_template = self.resolver.resolve_attribute(self.resources, ignore_errors=False) + + expected_resources = { + "HelloHandler2E4FBA4D": { + "Properties": {"handler": "main.handle"}, + "Type": "AWS::Lambda::Function", + }, + "LambdaFunction": { + "Properties": { + "Uri": "arn:aws:apigateway:us-east-1a:lambda:path/2015-03-31/functions/arn:aws" + ":lambda:us-east-1:406033500479:function:HelloHandler2E4FBA4D/invocations" + }, + "Type": "AWS::Lambda::Function", + }, + "ReferenceLambdaLayerVersionLambdaFunction": { + "Properties": { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{"Ref": "MyCustomLambdaLayer"}] + }, + "Type": "AWS::Serverless::Function", + }, + "MyCustomLambdaLayer": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": "custom_layer/" + } + }, + "RestApi": { + "Properties": { + "Body": "YTtlO2Y7ZA==", + "BodyS3Location": "https://s3location/", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiResource": { + "Properties": { + "PathPart": "{proxy+}", + "RestApiId": "RestApi", + "parentId": "/", + } + }, + } + self.assertEqual(dict(resolved_template), expected_resources) + + def test_template_fail_errors(self): + resources = deepcopy(self.resources) + resources["RestApi.Deployment"]["Properties"]["BodyS3Location"] = { + "Fn::FindInMap": [] + } + template = { + "Mappings": self.mappings, + "Conditions": self.conditions, + "Resources": resources, + } + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator=self.logical_id_translator + ) + resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) + with self.assertRaises(InvalidIntrinsicException, msg="Invalid Find In Map"): + resolver.resolve_attribute(resources, ignore_errors=False) + + def test_template_ignore_errors(self): + resources = deepcopy(self.resources) + resources["RestApi.Deployment"]["Properties"]["BodyS3Location"] = { + "Fn::FindInMap": [] + } + template = { + "Mappings": self.mappings, + "Conditions": self.conditions, + "Resources": resources, + } + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator=self.logical_id_translator + ) + resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) + result = resolver.resolve_attribute(resources, ignore_errors=True) + expected_template = { + "HelloHandler2E4FBA4D": { + "Properties": {"handler": "main.handle"}, + "Type": "AWS::Lambda::Function", + }, + "ReferenceLambdaLayerVersionLambdaFunction": { + "Properties": { + "Handler": "layer-main.custom_layer_handler", + "Runtime": "python3.6", + "CodeUri": ".", + "Layers": [{"Ref": "MyCustomLambdaLayer"}] + }, + "Type": "AWS::Serverless::Function", + }, + "MyCustomLambdaLayer": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": "custom_layer/" + } + }, + "LambdaFunction": { + "Properties": { + "Uri": "arn:aws:apigateway:us-east-1a:lambda:path/2015-03-31" + "/functions/arn:aws:lambda:us-east-1:406033500479" + ":function:HelloHandler2E4FBA4D/invocations" + }, + "Type": "AWS::Lambda::Function", + }, + "RestApi.Deployment": { + "Properties": { + "Body": { + "Fn::Base64": { + "Fn::Join": [ + ";", # NOQA + { + "Fn::Split": [ + ",", + {"Fn::Join": [",", ["a", "e", "f", "d"]]}, + ] + }, + ] + } + }, + "BodyS3Location": {"Fn::FindInMap": []}, + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "RestApiResource": { + "Properties": { + "PathPart": "{proxy+}", + "RestApiId": "RestApi", + "parentId": "/", + } + } + } + self.assertEqual(expected_template, dict(result)) + + +class TestResolveTemplate(TestCase): + def test_parameter_not_resolved(self): + template = { + "Parameters": { + "TestStageName": { + "Default": "test", + "Type": "string" + } + }, + "Resources": { + "Test": { + "Type": "AWS::ApiGateway::RestApi", + "Parameters": { + "StageName": { + "Ref": "TestStageName" + } + } + } + } + } + + expected_template = { + "Parameters": { + "TestStageName": { + "Default": "test", + "Type": "string" + }, + }, + "Resources": OrderedDict({ + "Test": { + "Type": "AWS::ApiGateway::RestApi", + "Parameters": { + "StageName": "test" + } + } + }) + } + + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator={} + ) + resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) + self.assertEqual(resolver.resolve_template(), expected_template) + + def test_mappings_directory_resolved(self): + template = { + "Mappings": { + "TestStageName": { + "TestKey": { + "key": "StageName" + } + } + }, + "Resources": { + "Test": { + "Type": "AWS::ApiGateway::RestApi", + "Parameters": { + "StageName": { + "Fn::FindInMap": ["TestStageName", "TestKey", "key"] + } + } + } + } + } + + expected_template = { + "Mappings": { + "TestStageName": { + "TestKey": { + "key": "StageName" + } + } + }, + "Resources": OrderedDict({ + "Test": { + "Type": "AWS::ApiGateway::RestApi", + "Parameters": { + "StageName": "StageName" + } + } + }) + } + + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator={} + ) + resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) + self.assertEqual(resolver.resolve_template(), expected_template) + + def test_output_resolved(self): + template = { + "Parameters": { + "StageRef": { + "Default": "StageName" + } + }, + "Outputs": { + "TestStageName": { + "Ref": "Test" + }, + "ParameterRef": { + "Ref": "StageRef" + } + }, + "Resources": { + "Test": { + "Type": "AWS::ApiGateway::RestApi", + "Parameters": { + "StageName": { + "Ref": "StageRef" + } + } + } + } + } + + expected_template = { + "Parameters": { + "StageRef": { + "Default": "StageName" + } + }, + "Resources": OrderedDict({ + "Test": { + "Type": "AWS::ApiGateway::RestApi", + "Parameters": { + "StageName": "StageName" + } + } + }), + "Outputs": OrderedDict({ + "TestStageName": "Test", + "ParameterRef": "StageName" + }) + } + + symbol_resolver = IntrinsicsSymbolTable( + template=template, logical_id_translator={} + ) + resolver = IntrinsicResolver(template=template, symbol_resolver=symbol_resolver) + self.assertEqual(resolver.resolve_template(), expected_template) + + def load_test_data(self, template_path): + integration_path = str( + Path(__file__).resolve().parents[0].joinpath('test_data', template_path)) + with open(integration_path) as f: + template = json.load(f) + return template + + @parameterized.expand([ + ('inputs/test_intrinsic_template_resolution.json', 'outputs/output_test_intrinsic_template_resolution.json'), + ('inputs/test_layers_resolution.json', 'outputs/outputs_test_layers_resolution.json'), + ('inputs/test_methods_resource_resolution.json', 'outputs/outputs_methods_resource_resolution.json'), + ]) + def test_intrinsic_sample_inputs_outputs(self, input, output): + input_template = self.load_test_data(input) + symbol_resolver = IntrinsicsSymbolTable( + template=input_template, logical_id_translator={} + ) + resolver = IntrinsicResolver(template=input_template, symbol_resolver=symbol_resolver) + processed_template = resolver.resolve_template() + processed_template = json.loads(json.dumps(processed_template)) # Removes formatting of ordered dicts + expected_template = self.load_test_data(output) + self.assertEqual(processed_template, expected_template) + + +class TestIntrinsicResolverInitialization(TestCase): + def test_conditional_key_function_map(self): + resolver = IntrinsicResolver(None, None) + + def lambda_func(x): + return True + + resolver.set_conditional_function_map({"key": lambda_func}) + self.assertTrue(resolver.conditional_key_function_map.get("key") == lambda_func) + + def test_set_intrinsic_key_function_map(self): + resolver = IntrinsicResolver(None, None) + + def lambda_func(x): + return True + + resolver.set_intrinsic_key_function_map({"key": lambda_func}) + self.assertTrue(resolver.intrinsic_key_function_map.get("key") == lambda_func) diff --git a/tests/unit/lib/intrinsic_resolver/test_intrinsics_symbol_table.py b/tests/unit/lib/intrinsic_resolver/test_intrinsics_symbol_table.py new file mode 100644 index 0000000000..6887b54e74 --- /dev/null +++ b/tests/unit/lib/intrinsic_resolver/test_intrinsics_symbol_table.py @@ -0,0 +1,152 @@ +from unittest import TestCase + +from mock import patch + +from samcli.lib.intrinsic_resolver.invalid_intrinsic_exception import InvalidSymbolException +from samcli.lib.intrinsic_resolver.intrinsic_property_resolver import IntrinsicResolver +from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable + + +class TestIntrinsicsSymbolTablePseudoProperties(TestCase): + def setUp(self): + self.symbol_table = IntrinsicsSymbolTable(template={}) + + def test_handle_account_id_default(self): + self.assertEquals(self.symbol_table.handle_pseudo_account_id(), "123456789012") + + def test_pseudo_partition(self): + self.assertEquals(self.symbol_table.handle_pseudo_partition(), "aws") + + @patch("samcli.lib.intrinsic_resolver.intrinsics_symbol_table.os") + def test_pseudo_partition_gov(self, mock_os): + mock_os.getenv.return_value = "us-west-gov-1" + self.assertEquals(self.symbol_table.handle_pseudo_partition(), "aws-us-gov") + + @patch("samcli.lib.intrinsic_resolver.intrinsics_symbol_table.os") + def test_pseudo_partition_china(self, mock_os): + mock_os.getenv.return_value = "cn-west-1" + self.assertEquals(self.symbol_table.handle_pseudo_partition(), "aws-cn") + + @patch("samcli.lib.intrinsic_resolver.intrinsics_symbol_table.os") + def test_pseudo_region_environ(self, mock_os): + mock_os.getenv.return_value = "mytemp" + self.assertEquals(self.symbol_table.handle_pseudo_region(), "mytemp") + + @patch("samcli.lib.intrinsic_resolver.intrinsics_symbol_table.os") + def test_pseudo_default_region(self, mock_os): + mock_os.getenv.return_value = None + self.assertEquals(self.symbol_table.handle_pseudo_region(), "us-east-1") + + def test_pseudo_no_value(self): + self.assertIsNone(self.symbol_table.handle_pseudo_no_value()) + + def test_pseudo_url_prefix_default(self): + self.assertEquals(self.symbol_table.handle_pseudo_url_prefix(), "amazonaws.com") + + @patch("samcli.lib.intrinsic_resolver.intrinsics_symbol_table.os") + def test_pseudo_url_prefix_china(self, mock_os): + mock_os.getenv.return_value = "cn-west-1" + self.assertEquals( + self.symbol_table.handle_pseudo_url_prefix(), "amazonaws.com.cn" + ) + + def test_get_availability_zone(self): + res = IntrinsicsSymbolTable.get_availability_zone("us-east-1") + self.assertIn("us-east-1a", res) + + def test_handle_pseudo_account_id(self): + res = IntrinsicsSymbolTable.handle_pseudo_account_id() + self.assertEqual(res, "123456789012") + + def test_handle_pseudo_stack_name(self): + res = IntrinsicsSymbolTable.handle_pseudo_stack_name() + self.assertEqual(res, "local") + + def test_handle_pseudo_stack_id(self): + res = IntrinsicsSymbolTable.handle_pseudo_stack_id() + self.assertEqual(res, "arn:aws:cloudformation:us-east-1:123456789012:stack/" + "local/51af3dc0-da77-11e4-872e-1234567db123") + + +class TestSymbolResolution(TestCase): + def test_parameter_symbols(self): + template = { + "Resources": {}, + "Parameters": { + "Test": { + "Default": "data" + } + } + } + symbol_resolver = IntrinsicsSymbolTable(template=template) + result = symbol_resolver.resolve_symbols("Test", IntrinsicResolver.REF) + self.assertEquals(result, "data") + + def test_default_type_resolver_function(self): + template = { + "Resources": { + "MyApi": { + "Type": "AWS::ApiGateway::RestApi" + } + }, + } + default_type_resolver = { + "AWS::ApiGateway::RestApi": { + "RootResourceId": lambda logical_id: logical_id + } + } + + symbol_resolver = IntrinsicsSymbolTable(template=template, default_type_resolver=default_type_resolver) + result = symbol_resolver.resolve_symbols("MyApi", "RootResourceId") + + self.assertEquals(result, "MyApi") + + def test_custom_attribute_resolver(self): + template = { + "Resources": { + "MyApi": { + "Type": "AWS::ApiGateway::RestApi" + } + }, + } + common_attribute_resolver = { + "Arn": "test" + } + + symbol_resolver = IntrinsicsSymbolTable(template=template, common_attribute_resolver=common_attribute_resolver) + result = symbol_resolver.resolve_symbols("MyApi", "Arn") + + self.assertEquals(result, "test") + + def test_unknown_symbol_translation(self): + symbol_resolver = IntrinsicsSymbolTable(template={}) + res = symbol_resolver.get_translation("UNKNOWN MAP") + self.assertEqual(res, None) + + def test_basic_symbol_translation(self): + symbol_resolver = IntrinsicsSymbolTable(template={}, logical_id_translator={"item": "test"}) + res = symbol_resolver.get_translation("item") + self.assertEqual(res, "test") + + def test_basic_unknown_translated_string_translation(self): + symbol_resolver = IntrinsicsSymbolTable(template={}, logical_id_translator={"item": "test"}) + res = symbol_resolver.get_translation("item", "RootResourceId") + self.assertEqual(res, None) + + def test_arn_resolver_lambda(self): + res = IntrinsicsSymbolTable().arn_resolver('test') + self.assertEquals(res, "arn:aws:lambda:us-east-1:123456789012:function:test") + + def test_arn_resolver(self): + res = IntrinsicsSymbolTable().arn_resolver('test', service_name="sns") + self.assertEquals(res, "arn:aws:sns:us-east-1:123456789012:test") + + def test_resolver_ignore_errors(self): + resolver = IntrinsicsSymbolTable() + res = resolver.resolve_symbols('UNKNOWN', "SOME UNKNOWN RESOURCE PROPERTY", ignore_errors=True) + self.assertEqual(res, "$UNKNOWN.SOME UNKNOWN RESOURCE PROPERTY") + + def test_symbol_resolver_unknown_fail(self): + resolver = IntrinsicsSymbolTable() + with self.assertRaises(InvalidSymbolException): + resolver.resolve_symbols('UNKNOWN', "SOME UNKNOWN RESOURCE PROPERTY") diff --git a/tests/unit/lib/samlib/test_cloudformation_command.py b/tests/unit/lib/samlib/test_cloudformation_command.py index f153530e9c..f6cd9f5889 100644 --- a/tests/unit/lib/samlib/test_cloudformation_command.py +++ b/tests/unit/lib/samlib/test_cloudformation_command.py @@ -151,7 +151,8 @@ def test_must_raise_error_if_executable_not_found(self, platform_system_mock, po with self.assertRaises(OSError) as ctx: find_executable(execname) - expected = "Unable to find AWS CLI installation under following names: {}".format(["foo.cmd", "foo.exe", "foo"]) + expected = "Cannot find AWS CLI installation, was looking at executables with names: {}".format( + ["foo.cmd", "foo.exe", "foo"]) self.assertEquals(expected, str(ctx.exception)) self.assertEquals(popen_mock.mock_calls, [ diff --git a/tests/unit/lib/telemetry/test_metrics.py b/tests/unit/lib/telemetry/test_metrics.py index 36365bf8ff..9ae585c9c7 100644 --- a/tests/unit/lib/telemetry/test_metrics.py +++ b/tests/unit/lib/telemetry/test_metrics.py @@ -113,7 +113,7 @@ def real_fn(): @patch("samcli.lib.telemetry.metrics.Context") def test_must_record_function_duration(self, ContextMock): ContextMock.get_current_context.return_value = self.context_mock - sleep_duration = 0.001 # 1 millisecond + sleep_duration = 0.01 # 10 millisecond def real_fn(): time.sleep(sleep_duration) @@ -125,9 +125,10 @@ def real_fn(): args, kwargs = self.telemetry_instance.emit.call_args_list[0] metric_name, actual_attrs = args self.assertEquals("commandRun", metric_name) - self.assertGreater(actual_attrs["duration"], - sleep_duration, - "Measured duration must be in milliseconds and greater than the sleep duration") + self.assertGreaterEqual(actual_attrs["duration"], + sleep_duration, + "Measured duration must be in milliseconds and " + "greater than equal to the sleep duration") @patch("samcli.lib.telemetry.metrics.Context") def test_must_record_user_exception(self, ContextMock): diff --git a/tests/unit/lib/utils/test_codeuri.py b/tests/unit/lib/utils/test_codeuri.py index 44dc8e9449..03652909b1 100644 --- a/tests/unit/lib/utils/test_codeuri.py +++ b/tests/unit/lib/utils/test_codeuri.py @@ -2,6 +2,11 @@ from unittest import TestCase from parameterized import parameterized +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + from samcli.lib.utils.codeuri import resolve_code_path @@ -52,7 +57,7 @@ def test_must_resolve_relative_codeuri(self, codeuri): expected = os.path.normpath(os.path.join(self.cwd, codeuri)) actual = resolve_code_path(self.cwd, codeuri) - self.assertEquals(expected, actual) + self.assertEquals(str(Path(expected).resolve()), actual) self.assertTrue(os.path.isabs(actual), "Result must be an absolute path") @parameterized.expand([ diff --git a/tests/unit/lib/utils/test_sam_logging.py b/tests/unit/lib/utils/test_sam_logging.py new file mode 100644 index 0000000000..c05f4b17ca --- /dev/null +++ b/tests/unit/lib/utils/test_sam_logging.py @@ -0,0 +1,25 @@ +from unittest import TestCase +from mock import patch, Mock + +from samcli.lib.utils.sam_logging import SamCliLogger + + +class TestSamCliLogger(TestCase): + + @patch("samcli.lib.utils.sam_logging.logging") + def test_configure_samcli_logger(self, logging_patch): + formatter_mock = Mock() + logger_mock = Mock() + logging_patch.DEBUG = 2 + + stream_handler_mock = Mock() + logging_patch.StreamHandler.return_value = stream_handler_mock + + SamCliLogger.configure_logger(logger_mock, formatter_mock, level=1) + + self.assertFalse(logger_mock.propagate) + + logger_mock.setLevel.assert_called_once_with(1) + logger_mock.addHandler.assert_called_once_with(stream_handler_mock) + stream_handler_mock.setLevel.assert_called_once_with(2) + stream_handler_mock.setFormatter.assert_called_once_with(formatter_mock) diff --git a/tests/unit/local/apigw/test_local_apigw_service.py b/tests/unit/local/apigw/test_local_apigw_service.py index ba2d6316b5..6ff3ee025d 100644 --- a/tests/unit/local/apigw/test_local_apigw_service.py +++ b/tests/unit/local/apigw/test_local_apigw_service.py @@ -1,11 +1,14 @@ -from unittest import TestCase -from mock import Mock, patch, ANY -import json import base64 +import copy +import json +from unittest import TestCase +from mock import Mock, patch, ANY, MagicMock from parameterized import parameterized, param from werkzeug.datastructures import Headers +from samcli.commands.local.lib.provider import Api +from samcli.commands.local.lib.provider import Cors from samcli.local.apigw.local_apigw_service import LocalApigwService, Route from samcli.local.lambdafn.exceptions import FunctionNotFound @@ -14,34 +17,39 @@ class TestApiGatewayService(TestCase): def setUp(self): self.function_name = Mock() - self.api_gateway_route = Route(['GET'], self.function_name, '/') + self.api_gateway_route = Route(methods=['GET'], function_name=self.function_name, path='/') self.list_of_routes = [self.api_gateway_route] self.lambda_runner = Mock() self.lambda_runner.is_debugging.return_value = False self.stderr = Mock() - self.service = LocalApigwService(self.list_of_routes, + self.api = Api(routes=self.list_of_routes) + self.service = LocalApigwService(self.api, self.lambda_runner, port=3000, host='127.0.0.1', stderr=self.stderr) - def test_request_must_invoke_lambda(self): + @patch.object(LocalApigwService, "get_request_methods_endpoints") + def test_request_must_invoke_lambda(self, request_mock): make_response_mock = Mock() self.service.service_response = make_response_mock - self.service._get_current_route = Mock() + self.service._get_current_route = MagicMock() + self.service._get_current_route.methods = [] self.service._construct_event = Mock() parse_output_mock = Mock() - parse_output_mock.return_value = ("status_code", "headers", "body") + parse_output_mock.return_value = ("status_code", Headers({"headers": "headers"}), "body") self.service._parse_lambda_output = parse_output_mock service_response_mock = Mock() service_response_mock.return_value = make_response_mock self.service.service_response = service_response_mock + request_mock.return_value = ('test', 'test') + result = self.service._request_handler() self.assertEquals(result, make_response_mock) @@ -50,16 +58,19 @@ def test_request_must_invoke_lambda(self): stdout=ANY, stderr=self.stderr) + @patch.object(LocalApigwService, "get_request_methods_endpoints") @patch('samcli.local.apigw.local_apigw_service.LambdaOutputParser') - def test_request_handler_returns_process_stdout_when_making_response(self, lambda_output_parser_mock): + def test_request_handler_returns_process_stdout_when_making_response(self, lambda_output_parser_mock, request_mock): make_response_mock = Mock() - + request_mock.return_value = ('test', 'test') self.service.service_response = make_response_mock - self.service._get_current_route = Mock() + self.service._get_current_route = MagicMock() + self.service._get_current_route.methods = [] + self.service._construct_event = Mock() parse_output_mock = Mock() - parse_output_mock.return_value = ("status_code", "headers", "body") + parse_output_mock.return_value = ("status_code", Headers({"headers": "headers"}), "body") self.service._parse_lambda_output = parse_output_mock lambda_logs = "logs" @@ -80,21 +91,24 @@ def test_request_handler_returns_process_stdout_when_making_response(self, lambd # Make sure the logs are written to stderr self.stderr.write.assert_called_with(lambda_logs) - def test_request_handler_returns_make_response(self): + @patch.object(LocalApigwService, "get_request_methods_endpoints") + def test_request_handler_returns_make_response(self, request_mock): make_response_mock = Mock() self.service.service_response = make_response_mock - self.service._get_current_route = Mock() + self.service._get_current_route = MagicMock() self.service._construct_event = Mock() + self.service._get_current_route.methods = [] parse_output_mock = Mock() - parse_output_mock.return_value = ("status_code", "headers", "body") + parse_output_mock.return_value = ("status_code", Headers({"headers": "headers"}), "body") self.service._parse_lambda_output = parse_output_mock service_response_mock = Mock() service_response_mock.return_value = make_response_mock self.service.service_response = service_response_mock + request_mock.return_value = ('test', 'test') result = self.service._request_handler() self.assertEquals(result, make_response_mock) @@ -102,14 +116,15 @@ def test_request_handler_returns_make_response(self): def test_create_creates_dict_of_routes(self): function_name_1 = Mock() function_name_2 = Mock() - api_gateway_route_1 = Route(['GET'], function_name_1, '/') - api_gateway_route_2 = Route(['POST'], function_name_2, '/') + api_gateway_route_1 = Route(methods=["GET"], function_name=function_name_1, path='/') + api_gateway_route_2 = Route(methods=["POST"], function_name=function_name_2, path='/') list_of_routes = [api_gateway_route_1, api_gateway_route_2] lambda_runner = Mock() - service = LocalApigwService(list_of_routes, lambda_runner) + api = Api(routes=list_of_routes) + service = LocalApigwService(api, lambda_runner) service.create() @@ -135,43 +150,50 @@ def test_create_creates_flask_app_with_url_rules(self, flask): def test_initalize_creates_default_values(self): self.assertEquals(self.service.port, 3000) self.assertEquals(self.service.host, '127.0.0.1') - self.assertEquals(self.service.routing_list, self.list_of_routes) + self.assertEquals(self.service.api.routes, self.list_of_routes) self.assertIsNone(self.service.static_dir) self.assertEquals(self.service.lambda_runner, self.lambda_runner) def test_initalize_with_values(self): lambda_runner = Mock() - local_service = LocalApigwService([], lambda_runner, static_dir='dir/static', port=5000, host='129.0.0.0') + local_service = LocalApigwService(Api(), lambda_runner, static_dir='dir/static', port=5000, host='129.0.0.0') self.assertEquals(local_service.port, 5000) self.assertEquals(local_service.host, '129.0.0.0') - self.assertEquals(local_service.routing_list, []) + self.assertEquals(local_service.api.routes, []) self.assertEquals(local_service.static_dir, 'dir/static') self.assertEquals(local_service.lambda_runner, lambda_runner) + @patch.object(LocalApigwService, "get_request_methods_endpoints") @patch('samcli.local.apigw.local_apigw_service.ServiceErrorResponses') - def test_request_handles_error_when_invoke_cant_find_function(self, service_error_responses_patch): + def test_request_handles_error_when_invoke_cant_find_function(self, service_error_responses_patch, request_mock): not_found_response_mock = Mock() self.service._construct_event = Mock() - self.service._get_current_route = Mock() + self.service._get_current_route = MagicMock() + self.service._get_current_route.methods = [] + service_error_responses_patch.lambda_not_found_response.return_value = not_found_response_mock self.lambda_runner.invoke.side_effect = FunctionNotFound() - + request_mock.return_value = ('test', 'test') response = self.service._request_handler() self.assertEquals(response, not_found_response_mock) - def test_request_throws_when_invoke_fails(self): + @patch.object(LocalApigwService, "get_request_methods_endpoints") + def test_request_throws_when_invoke_fails(self, request_mock): self.lambda_runner.invoke.side_effect = Exception() self.service._construct_event = Mock() self.service._get_current_route = Mock() + request_mock.return_value = ('test', 'test') with self.assertRaises(Exception): self.service._request_handler() + @patch.object(LocalApigwService, "get_request_methods_endpoints") @patch('samcli.local.apigw.local_apigw_service.ServiceErrorResponses') - def test_request_handler_errors_when_parse_lambda_output_raises_keyerror(self, service_error_responses_patch): + def test_request_handler_errors_when_parse_lambda_output_raises_keyerror(self, service_error_responses_patch, + request_mock): parse_output_mock = Mock() parse_output_mock.side_effect = KeyError() self.service._parse_lambda_output = parse_output_mock @@ -181,8 +203,10 @@ def test_request_handler_errors_when_parse_lambda_output_raises_keyerror(self, s service_error_responses_patch.lambda_failure_response.return_value = failure_response_mock self.service._construct_event = Mock() - self.service._get_current_route = Mock() + self.service._get_current_route = MagicMock() + self.service._get_current_route.methods = [] + request_mock.return_value = ('test', 'test') result = self.service._request_handler() self.assertEquals(result, failure_response_mock) @@ -196,16 +220,20 @@ def test_request_handler_errors_when_get_current_route_fails(self, service_error with self.assertRaises(KeyError): self.service._request_handler() + @patch.object(LocalApigwService, "get_request_methods_endpoints") @patch('samcli.local.apigw.local_apigw_service.ServiceErrorResponses') - def test_request_handler_errors_when_unable_to_read_binary_data(self, service_error_responses_patch): + def test_request_handler_errors_when_unable_to_read_binary_data(self, service_error_responses_patch, request_mock): _construct_event = Mock() _construct_event.side_effect = UnicodeDecodeError("utf8", b"obj", 1, 2, "reason") - self.service._get_current_route = Mock() + self.service._get_current_route = MagicMock() + self.service._get_current_route.methods = [] + self.service._construct_event = _construct_event failure_mock = Mock() service_error_responses_patch.lambda_failure_response.return_value = failure_mock + request_mock.return_value = ('test', 'test') result = self.service._request_handler() self.assertEquals(result, failure_mock) @@ -250,19 +278,12 @@ class TestApiGatewayModel(TestCase): def setUp(self): self.function_name = "name" - self.stage_name = "Dev" - self.stage_variables = { - "test": "sample" - } - self.api_gateway = Route(['POST'], self.function_name, '/', stage_name=self.stage_name, - stage_variables=self.stage_variables) + self.api_gateway = Route(function_name=self.function_name, methods=["Post"], path="/") def test_class_initialization(self): self.assertEquals(self.api_gateway.methods, ['POST']) self.assertEquals(self.api_gateway.function_name, self.function_name) self.assertEquals(self.api_gateway.path, '/') - self.assertEqual(self.api_gateway.stage_name, "Dev") - self.assertEqual(self.api_gateway.stage_variables, {"test": "sample"}) class TestLambdaHeaderDictionaryMerge(TestCase): @@ -488,7 +509,7 @@ def setUp(self): '"Custom User Agent String", "caller": null, "cognitoAuthenticationType": null, "sourceIp": ' \ '"190.0.0.0", "user": null}, "accountId": "123456789012"}, "headers": {"Content-Type": ' \ '"application/json", "X-Test": "Value", "X-Forwarded-Port": "3000", "X-Forwarded-Proto": "http"}, ' \ - '"multiValueHeaders": {"Content-Type": ["application/json"], "X-Test": ["Value"], '\ + '"multiValueHeaders": {"Content-Type": ["application/json"], "X-Test": ["Value"], ' \ '"X-Forwarded-Port": ["3000"], "X-Forwarded-Proto": ["http"]}, ' \ '"stageVariables": null, "path": "path", "pathParameters": {"path": "params"}, ' \ '"isBase64Encoded": false}' @@ -590,3 +611,78 @@ def test_should_base64_encode_returns_true(self, test_case_name, binary_types, m ]) def test_should_base64_encode_returns_false(self, test_case_name, binary_types, mimetype): self.assertFalse(LocalApigwService._should_base64_encode(binary_types, mimetype)) + + +class TestServiceCorsToHeaders(TestCase): + def test_basic_conversion(self): + cors = Cors(allow_origin="*", allow_methods=','.join(["POST", "OPTIONS"]), allow_headers="UPGRADE-HEADER", + max_age=6) + headers = Cors.cors_to_headers(cors) + + self.assertEquals(headers, {'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST,OPTIONS', + 'Access-Control-Allow-Headers': 'UPGRADE-HEADER', 'Access-Control-Max-Age': 6}) + + def test_empty_elements(self): + cors = Cors(allow_origin="www.domain.com", allow_methods=','.join(["GET", "POST", "OPTIONS"])) + headers = Cors.cors_to_headers(cors) + + self.assertEquals(headers, + {'Access-Control-Allow-Origin': 'www.domain.com', + 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS'}) + + +class TestRouteEqualsHash(TestCase): + + def test_route_in_list(self): + route = Route(function_name="test", path="/test", methods=["POST"]) + routes = [route] + self.assertIn(route, routes) + + def test_route_method_order_equals(self): + route1 = Route(function_name="test", path="/test", methods=["POST", "GET"]) + route2 = Route(function_name="test", path="/test", methods=["GET", "POST"]) + self.assertEquals(route1, route2) + + def test_route_hash(self): + route1 = Route(function_name="test", path="/test", methods=["POST", "GET"]) + dic = {route1: "test"} + self.assertEquals(dic[route1], "test") + + def test_route_object_equals(self): + route1 = Route(function_name="test", path="/test", methods=["POST", "GET"]) + route2 = type('obj', (object,), {'function_name': 'test', "path": "/test", "methods": ["GET", "POST"]}) + + self.assertNotEqual(route1, route2) + + def test_route_function_name_equals(self): + route1 = Route(function_name="test1", path="/test", methods=["GET", "POST"]) + route2 = Route(function_name="test2", path="/test", methods=["GET", "POST"]) + self.assertNotEqual(route1, route2) + + def test_route_different_path_equals(self): + route1 = Route(function_name="test", path="/test1", methods=["GET", "POST"]) + route2 = Route(function_name="test", path="/test2", methods=["GET", "POST"]) + self.assertNotEqual(route1, route2) + + def test_same_object_equals(self): + route1 = Route(function_name="test", path="/test", methods=["POST", "GET"]) + self.assertEquals(route1, copy.deepcopy(route1)) + + def test_route_function_name_hash(self): + route1 = Route(function_name="test1", path="/test", methods=["GET", "POST"]) + route2 = Route(function_name="test2", path="/test", methods=["GET", "POST"]) + self.assertNotEqual(route1.__hash__(), route2.__hash__()) + + def test_route_different_path_hash(self): + route1 = Route(function_name="test", path="/test1", methods=["GET", "POST"]) + route2 = Route(function_name="test", path="/test2", methods=["GET", "POST"]) + self.assertNotEqual(route1.__hash__(), route2.__hash__()) + + def test_same_object_hash(self): + route1 = Route(function_name="test", path="/test", methods=["POST", "GET"]) + self.assertEquals(route1.__hash__(), copy.deepcopy(route1).__hash__()) + + def test_route_method_order_hash(self): + route1 = Route(function_name="test", path="/test", methods=["POST", "GET"]) + route2 = Route(function_name="test", path="/test", methods=["GET", "POST"]) + self.assertEquals(route1.__hash__(), route2.__hash__()) diff --git a/tests/unit/local/docker/test_lambda_build_container.py b/tests/unit/local/docker/test_lambda_build_container.py index 66901962fe..f51e8589be 100644 --- a/tests/unit/local/docker/test_lambda_build_container.py +++ b/tests/unit/local/docker/test_lambda_build_container.py @@ -8,10 +8,11 @@ except ImportError: import pathlib2 as pathlib - from unittest import TestCase from mock import patch +from parameterized import parameterized + from samcli.local.docker.lambda_build_container import LambdaBuildContainer @@ -54,10 +55,10 @@ def test_must_init_class(self, self.assertEquals(container._entrypoint, entry) self.assertEquals(container._cmd, []) self.assertEquals(container._working_dir, container_dirs["source_dir"]) - self.assertEquals(container._host_dir, "/foo/source") + self.assertEquals(container._host_dir, str(pathlib.Path("/foo/source").resolve())) self.assertEquals(container._env_vars, {"LAMBDA_BUILDERS_LOG_LEVEL": "log-level"}) self.assertEquals(container._additional_volumes, { - "/bar": { + str(pathlib.Path("/bar").resolve()): { "bind": container_dirs["manifest_dir"], "mode": "ro" } @@ -71,7 +72,8 @@ def test_must_init_class(self, make_request_mock.assert_called_once() get_entrypoint_mock.assert_called_once_with(request) get_image_mock.assert_called_once_with("runtime") - get_container_dirs_mock.assert_called_once_with("/foo/source", "/bar") + get_container_dirs_mock.assert_called_once_with(str(pathlib.Path("/foo/source").resolve()), + str(pathlib.Path("/bar").resolve())) class TestLambdaBuildContainer_make_request(TestCase): @@ -156,8 +158,12 @@ def test_must_override_manifest_if_equal_to_source(self): class TestLambdaBuildContainer_get_image(TestCase): - def test_must_get_image_name(self): - self.assertEquals("lambci/lambda:build-myruntime", LambdaBuildContainer._get_image("myruntime")) + @parameterized.expand([ + ("myruntime", "lambci/lambda:build-myruntime"), + ("nodejs10.x", "amazon/lambda-build-node10.x") + ]) + def test_must_get_image_name(self, runtime, expected_image_name): + self.assertEquals(expected_image_name, LambdaBuildContainer._get_image(runtime)) class TestLambdaBuildContainer_get_entrypoint(TestCase): diff --git a/tests/unit/local/lambdafn/test_zip.py b/tests/unit/local/lambdafn/test_zip.py index 2b8dfb355d..ce0feea079 100644 --- a/tests/unit/local/lambdafn/test_zip.py +++ b/tests/unit/local/lambdafn/test_zip.py @@ -1,19 +1,24 @@ -import stat -import zipfile import os +import platform import shutil -from unittest import TestCase +import stat +import zipfile from contextlib import contextmanager from tempfile import NamedTemporaryFile, mkdtemp -from mock import Mock, patch +from unittest import TestCase +from unittest import skipIf +from mock import Mock, patch from nose_parameterized import parameterized, param from samcli.local.lambdafn.zip import unzip, unzip_from_uri, _override_permissions +# On Windows, permissions do not match 1:1 with permissions on Unix systems. +SKIP_UNZIP_PERMISSION_TESTS = platform.system() == 'Windows' -class TestUnzipWithPermissions(TestCase): +@skipIf(SKIP_UNZIP_PERMISSION_TESTS, "Skip UnZip Permissions tests in Windows only") +class TestUnzipWithPermissions(TestCase): files_with_permissions = { "folder1/1.txt": 0o644, "folder1/2.txt": 0o777, diff --git a/tests/unit/local/layers/test_download_layers.py b/tests/unit/local/layers/test_download_layers.py index 92286a6fde..8abe57dcb4 100644 --- a/tests/unit/local/layers/test_download_layers.py +++ b/tests/unit/local/layers/test_download_layers.py @@ -2,6 +2,11 @@ from mock import patch, Mock, call from botocore.exceptions import NoCredentialsError, ClientError +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + from samcli.local.layers.layer_downloader import LayerDownloader from samcli.commands.local.cli_common.user_exceptions import CredentialsRequired, ResourceNotFound @@ -55,7 +60,7 @@ def test_download_layer_that_is_cached(self, is_layer_cached_patch, create_cache actual = download_layers.download(layer_mock) - self.assertEquals(actual.codeuri, '/home/layer1') + self.assertEquals(actual.codeuri, str(Path('/home/layer1').resolve())) create_cache_patch.assert_called_once_with("/home") @@ -99,13 +104,13 @@ def test_download_layer(self, is_layer_cached_patch, create_cache_patch, actual = download_layers.download(layer_mock) - self.assertEquals(actual.codeuri, "/home/layer1") + self.assertEquals(actual.codeuri, str(Path("/home/layer1").resolve())) create_cache_patch.assert_called_once_with("/home") fetch_layer_uri_patch.assert_called_once_with(layer_mock) unzip_from_uri_patch.assert_called_once_with("layer/uri", - '/home/layer1.zip', - unzip_output_dir='/home/layer1', + str(Path('/home/layer1.zip').resolve()), + unzip_output_dir=str(Path('/home/layer1').resolve()), progressbar_label="Downloading arn:layer:layer1") def test_layer_is_cached(self):