diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..27d29d044b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Create an issue to report a bug for the SAM Translator +title: '' +labels: '' +assignees: '' + +--- + + + +### Description: + + + + +### Steps to reproduce: + + + + +### Observed result: + + + + +### Expected result: + + + + +### Additional environment details (Ex: Windows, Mac, Amazon Linux etc) + +1. OS: +2. If using SAM CLI, `sam --version`: +3. AWS region: + +`Add --debug flag to any SAM CLI commands you are running` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..144feb3fb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea/feature/enhancement for the SAM Translator +title: '' +labels: '' +assignees: '' + +--- + + + +### Describe your idea/feature/enhancement + +Provide a clear description. + +Ex: I wish the SAM Translator would [...] + +### Proposal + +Add details on how to add this to the product. + +Things to consider: +[ ] The [SAM documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification.html) will need to be updated + +### Additional Details diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md index 6ccbb9ad67..de44b115c6 100644 --- a/DEVELOPMENT_GUIDE.md +++ b/DEVELOPMENT_GUIDE.md @@ -128,6 +128,10 @@ will not work in Python3.6). If you want to test in many versions, you can creat each version and flip between them (sourcing the activate script). Typically, we run all tests in one python version locally and then have our ci (appveyor) run all supported versions. +### Integration tests + +Integration tests are covered in detail in the [INTEGRATION_TESTS.md file](INTEGRATION_TESTS.md) of this repository. + Code Conventions ---------------- diff --git a/INTEGRATION_TESTS.md b/INTEGRATION_TESTS.md new file mode 100644 index 0000000000..7e92667a15 --- /dev/null +++ b/INTEGRATION_TESTS.md @@ -0,0 +1,131 @@ +# AWS SAM integration tests + +These tests run SAM against AWS services by translating SAM templates, deploying them to Cloud Formation and verifying the resulting objects. + +They must run successfully under Python 2 and 3. + +## Run the tests + +### Prerequisites + +#### User and rights + +An Internet connection and an active AWS account are required to run the tests as they will interact with AWS services (most notably Cloud Formation) to create and update objects (Stacks, APIs, ...). + +AWS credentials must be configured either through a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) or [environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) + +The user running the tests must have the following roles: + +``` +AmazonSQSFullAccess +AmazonSNSFullAccess +AmazonAPIGatewayAdministrator +AWSKeyManagementServiceFullAccess +AWSStepFunctionsFullAccess +``` + +If you plan on running the full tests suite, ensure that the user credentials you are running the tests with have a timeout of at least 30 minutes as the full suite can take more than 20 minutes to execute. + +#### Initialize the development environment + +If you haven't done so already, run the following command in a terminal at the root of the repository to initialize the development environment: + +``` +make init +``` + +### Running all the tests + +From the root of the repository, run: + +``` +make integ-test +``` + +### Running a specific test file + +From the command line, run: + +``` +pytest --no-cov path/to/the/test_file.py +``` + +For example, from the root of the project: + +```sh +pytest --no-cov integration/single/test_basic_api.py +``` + +### Running a specific test + +From the command line, run: + +``` +pytest --no-cov path/to/the/test_file.py::test_class::test_method +``` + +For example, from the root of the project: + +```sh +pytest --no-cov integration/single/test_basic_api.py::TestBasicApi::test_basic_api +``` + +*We don't measure coverage for integration tests.* + +## Write a test + +1. Add your test templates to the `integration/resources/templates` single or combination folder. +2. Write an expected json file for all the expected resources and add it to the `integration/resources/expected`. +3. (Optional) Add the resource files (zip, json, etc.) to `integration/resources/code` and update the dictionaries in `integration/helpers/file_resources.py`. +4. Write and add your python test code to the `integration` single or combination folder. +5. Run it! + +## Skip tests for a specific service in a region + +1. Add the service you want to skip to the `integration/config/region_service_exclusion.yaml` under the region +2. Add the @skipIf decorator to the test with the service name, take 'XRay' for example: +```@skipIf(current_region_does_not_support('XRay'), 'XRay is not supported in this testing region')``` + +## Directory structure + +### Helpers + +Common classes and tools used by tests. + +``` ++-- helpers/ +| +-- deployer Tools to deploy to Cloud Formation +| +-- base_test.py Common class from which all test classes inherit +| +-- file_resources.py Files to upload to S3 +| +-- resource.py Helper functions to manipulate resources +| +-- template.py Helper functions to translate the template +``` + +`base_test.py` contains `setUpClass` and `tearDownClass` methods to respectively upload and clean the `file_resources.py` resources (upload the files to a new S3 bucket, empty and delete this bucket). + +### Resources + +File resources used by tests. + +``` ++-- resources +| +-- code Files to upload to S3 +| +-- expected Files describing the expected created resources +| +-- templates Source SAM templates to translate and deploy +``` + +The matching *expected* and *template* files should have the same name. + +For example, the `test_basic_api` test in the class `tests_integ/single/test_basic_api.py` takes `templates/single/basic_api.yaml` SAM template as input and verifies its result against `expected/single/basic_api.json`. + +### Single + +Basic tests which interact with only one service should be put here. + +### Combination + +Tests which interact with multiple services should be put there. + +### Tmp + +This directory is created on the first run and contains temporary and intermediary files used by the tests: sam templates with substituted variable values, translated temporary cloud formation templates, ... diff --git a/Makefile b/Makefile index c2b980b8f7..09dce252d5 100755 --- a/Makefile +++ b/Makefile @@ -6,13 +6,16 @@ init: pip install -e '.[dev]' test: - pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests + pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* + +integ-test: + pytest --no-cov integration/* black: - black setup.py samtranslator/* tests/* bin/*.py + black setup.py samtranslator/* tests/* integration/* bin/*.py black-check: - black --check setup.py samtranslator/* tests/* bin/*.py + black --check setup.py samtranslator/* tests/* integration/* bin/*.py # Command to run everytime you make changes to verify everything works dev: test @@ -30,6 +33,7 @@ Usage: $ make [TARGETS] TARGETS init Initialize and install the requirements and dev-requirements for this project. test Run the Unit tests. + integ-test Run the Integration tests. dev Run all development tests after a change. pr Perform all checks before submitting a Pull Request. diff --git a/README.md b/README.md index 0b5f186bdf..09e8c5638f 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,19 @@ # AWS Serverless Application Model (AWS SAM) -![Apache-2.0](https://img.shields.io/github/license/awslabs/serverless-application-model.svg) -![SAM_CLI release](https://img.shields.io/github/release/awslabs/aws-sam-cli.svg?label=CLI%20Version) +![Apache-2.0](https://img.shields.io/github/license/aws/serverless-application-model.svg) +![SAM_CLI release](https://img.shields.io/github/release/aws/aws-sam-cli.svg?label=CLI%20Version) +[![codecov](https://codecov.io/gh/aws/serverless-application-model/branch/master/graphs/badge.svg?style=flat)](https://codecov.io/gh/aws/serverless-application-model) -The AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications. -It provides shorthand syntax to express functions, APIs, databases, and event source mappings. +The AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications. +It provides shorthand syntax to express functions, APIs, databases, and event source mappings. With just a few lines of configuration, you can define the application you want and model it. [![Getting Started with AWS SAM](./docs/get-started-youtube.png)](https://www.youtube.com/watch?v=1dzihtC5LJ0) ## Get Started -To get started with building SAM-based applications, use the SAM CLI. SAM CLI provides a Lambda-like execution +To get started with building SAM-based applications, use the SAM CLI. SAM CLI provides a Lambda-like execution environment that lets you locally build, test, debug, and deploy applications defined by SAM templates. * [Install SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) @@ -35,14 +36,14 @@ environment that lets you locally build, test, debug, and deploy applications de ## Why SAM + **Single\-deployment configuration**\. SAM makes it easy to organize related components and resources, and operate on a single stack\. You can use SAM to share configuration \(such as memory and timeouts\) between resources, and deploy all related resources together as a single, versioned entity\. - + + **Local debugging and testing**\. Use SAM CLI to locally build, test, and debug SAM applications on a Lambda-like execution environment. It tightens the development loop by helping you find & troubleshoot issues locally that you might otherwise identify only after deploying to the cloud. + **Deep integration with development tools**. You can use SAM with a suite of tools you love and use. + IDEs: [PyCharm](https://aws.amazon.com/pycharm/), [IntelliJ](https://aws.amazon.com/intellij/), [Visual Studio Code](https://aws.amazon.com/visualstudiocode/), [Visual Studio](https://aws.amazon.com/visualstudio/), [AWS Cloud9](https://aws.amazon.com/cloud9/) + Build: [CodeBuild](https://docs.aws.amazon.com/codebuild/latest/userguide/) + Deploy: [CodeDeploy](https://docs.aws.amazon.com/codedeploy/latest/userguide/), [Jenkins](https://wiki.jenkins.io/display/JENKINS/AWS+SAM+Plugin) - + Continuous Delivery Pipelines: [CodePipeline](https://docs.aws.amazon.com/codepipeline/latest/userguide/) + + Continuous Delivery Pipelines: [CodePipeline](https://docs.aws.amazon.com/codepipeline/latest/userguide/) + Discover Serverless Apps & Patterns: [AWS Serverless Application Repository](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/) + **Built\-in best practices**\. You can use SAM to define and deploy your infrastructure as configuration. This makes it possible for you to use and enforce best practices through code reviews. Also, with a few lines of configuration, you can enable safe deployments through CodeDeploy, and can enable tracing using AWS X\-Ray\. @@ -51,40 +52,40 @@ environment that lets you locally build, test, debug, and deploy applications de ## What is this Github repository? 💻 -This GitHub repository contains the SAM Specification, the Python code that translates SAM templates into AWS CloudFormation stacks and lots of examples applications. +This GitHub repository contains the SAM Specification, the Python code that translates SAM templates into AWS CloudFormation stacks and lots of examples applications. In the words of SAM developers: -> SAM Translator is the Python code that deploys SAM templates via AWS CloudFormation. Source code is high quality (95% unit test coverage), -with tons of tests to ensure your changes don't break compatibility. Change the code, run the tests, and if they pass, you should be good to go! +> SAM Translator is the Python code that deploys SAM templates via AWS CloudFormation. Source code is high quality (95% unit test coverage), +with tons of tests to ensure your changes don't break compatibility. Change the code, run the tests, and if they pass, you should be good to go! Clone it and run `make pr`! ## Contribute to SAM -We love our contributors ❤️ We have over 100 contributors who have built various parts of the product. +We love our contributors ❤️ We have over 100 contributors who have built various parts of the product. Read this [testimonial from @ndobryanskyy](https://www.lohika.com/aws-sam-my-exciting-first-open-source-experience/) to learn -more about what it was like contributing to SAM. +more about what it was like contributing to SAM. -Depending on your interest and skill, you can help build the different parts of the SAM project; +Depending on your interest and skill, you can help build the different parts of the SAM project; **Enhance the SAM Specification** Make pull requests, report bugs, and share ideas to improve the full SAM template specification. -Source code is located on Github at [awslabs/serverless-application-model](https://github.com/awslabs/serverless-application-model). +Source code is located on Github at [awslabs/serverless-application-model](https://github.com/awslabs/serverless-application-model). Read the [SAM Specification Contributing Guide](https://github.com/awslabs/serverless-application-model/blob/master/CONTRIBUTING.md) to get started. - + **Strengthen SAM CLI** Add new commands or enhance existing ones, report bugs, or request new features for the SAM CLI. -Source code is located on Github at [awslabs/aws-sam-cli](https://github.com/awslabs/aws-sam-cli). Read the [SAM CLI Contributing Guide](https://github.com/awslabs/aws-sam-cli/blob/develop/CONTRIBUTING.md) to -get started. +Source code is located on Github at [awslabs/aws-sam-cli](https://github.com/awslabs/aws-sam-cli). Read the [SAM CLI Contributing Guide](https://github.com/awslabs/aws-sam-cli/blob/develop/CONTRIBUTING.md) to +get started. **Update SAM Developer Guide** [SAM Developer Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/index.html) provides comprehensive getting started guide and reference documentation. Source code is located on Github at [awsdocs/aws-sam-developer-guide](https://github.com/awsdocs/aws-sam-developer-guide). Read the [SAM Documentation Contribution Guide](https://github.com/awsdocs/aws-sam-developer-guide/blob/master/CONTRIBUTING.md) to get -started. +started. ### Join the SAM Community on Slack [Join the SAM developers channel (#samdev)](https://join.slack.com/t/awsdevelopers/shared_invite/zt-idww18e8-Z1kXhI7GNuDewkweCF3YjA) on Slack to collaborate with fellow community members and the AWS SAM team. diff --git a/appveyor-integration-test.yml b/appveyor-integration-test.yml new file mode 100644 index 0000000000..0dc1cdf12c --- /dev/null +++ b/appveyor-integration-test.yml @@ -0,0 +1,24 @@ +version: 1.0.{build} +image: Ubuntu + +environment: + matrix: + - TOXENV: py27 + PYTHON_VERSION: '2.7' + - TOXENV: py36 + PYTHON_VERSION: '3.6' + - TOXENV: py37 + PYTHON_VERSION: '3.7' + - TOXENV: py38 + PYTHON_VERSION: '3.8' + +build: off + +install: +- sh: "source ${HOME}/venv${PYTHON_VERSION}/bin/activate" +- sh: "python --version" +- make init + +test_script: +- make integ-test + diff --git a/integration/__init__.py b/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/config/__init__.py b/integration/config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/config/region_service_exclusion.yaml b/integration/config/region_service_exclusion.yaml new file mode 100644 index 0000000000..a7e63ee424 --- /dev/null +++ b/integration/config/region_service_exclusion.yaml @@ -0,0 +1,86 @@ +regions: + af-south-1: + - ServerlessRepo + - Cognito + - KMS + - CodeDeploy + - XRay + - IoT + - GatewayResponses + - HttpApi + ap-east-1: + - Cognito + - IoT + - ServerlessRepo + - HttpApi + ap-northeast-2: + - HttpApi + ap-northeast-3: + - Cognito + - IoT + - ServerlessRepo + - XRay + - CodeDeploy + - HttpApi + ap-south-1: + - HttpApi + ap-southeast-1: + - HttpApi + ca-central-1: + - Cognito + - IoT + - HttpApi + cn-north-1: + - ServerlessRepo + - Cognito + - KMS + - CodeDeploy + - XRay + - IoT + - GatewayResponses + - HttpApi + cn-northwest-1: + - ServerlessRepo + - Cognito + - KMS + - CodeDeploy + - XRay + - IoT + - GatewayResponses + - HttpApi + eu-north-1: + - ServerlessRepo + - Cognito + - IoT + - HttpApi + - Layers + eu-south-1: + - ServerlessRepo + - Cognito + - KMS + - CodeDeploy + - XRay + - IoT + - GatewayResponses + - HttpApi + eu-west-2: + - HttpApi + eu-west-3: + - Cognito + - IoT + - XRay + - HttpApi + me-south-1: + - ServerlessRepo + - Cognito + - IoT + - HttpApi + sa-east-1: + - IoT + - Cognito + - HttpApi + us-east-2: + - HttpApi + us-west-1: + - Cognito + - IoT diff --git a/integration/helpers/__init__.py b/integration/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/helpers/base_test.py b/integration/helpers/base_test.py new file mode 100644 index 0000000000..58b654705b --- /dev/null +++ b/integration/helpers/base_test.py @@ -0,0 +1,353 @@ +import logging +import os + +from integration.helpers.client_provider import ClientProvider +from integration.helpers.resource import generate_suffix, create_bucket, verify_stack_resources +from integration.helpers.yaml_utils import dump_yaml, load_yaml + +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path +from unittest.case import TestCase + +import boto3 +import pytest +import yaml +from botocore.exceptions import ClientError +from botocore.config import Config +from integration.helpers.deployer.deployer import Deployer +from integration.helpers.template import transform_template + +from integration.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP + +LOG = logging.getLogger(__name__) +STACK_NAME_PREFIX = "sam-integ-stack-" +S3_BUCKET_PREFIX = "sam-integ-bucket-" + + +class BaseTest(TestCase): + @classmethod + def setUpClass(cls): + cls.FUNCTION_OUTPUT = "hello" + cls.tests_integ_dir = Path(__file__).resolve().parents[1] + cls.resources_dir = Path(cls.tests_integ_dir, "resources") + cls.template_dir = Path(cls.resources_dir, "templates", "single") + cls.output_dir = Path(cls.tests_integ_dir, "tmp") + cls.expected_dir = Path(cls.resources_dir, "expected", "single") + cls.code_dir = Path(cls.resources_dir, "code") + cls.s3_bucket_name = S3_BUCKET_PREFIX + generate_suffix() + cls.session = boto3.session.Session() + cls.my_region = cls.session.region_name + cls.client_provider = ClientProvider() + cls.file_to_s3_uri_map = FILE_TO_S3_URI_MAP + cls.code_key_to_file = CODE_KEY_TO_FILE_MAP + + if not cls.output_dir.exists(): + os.mkdir(str(cls.output_dir)) + + cls._upload_resources(FILE_TO_S3_URI_MAP) + + @classmethod + def tearDownClass(cls): + cls._clean_bucket() + + @classmethod + def _clean_bucket(cls): + """ + Empties and deletes the bucket used for the tests + """ + s3 = boto3.resource("s3") + bucket = s3.Bucket(cls.s3_bucket_name) + object_summary_iterator = bucket.objects.all() + + for object_summary in object_summary_iterator: + try: + cls.client_provider.s3_client.delete_object(Key=object_summary.key, Bucket=cls.s3_bucket_name) + except ClientError as e: + LOG.error( + "Unable to delete object %s from bucket %s", object_summary.key, cls.s3_bucket_name, exc_info=e + ) + try: + cls.client_provider.s3_client.delete_bucket(Bucket=cls.s3_bucket_name) + except ClientError as e: + LOG.error("Unable to delete bucket %s", cls.s3_bucket_name, exc_info=e) + + @classmethod + def _upload_resources(cls, file_to_s3_uri_map): + """ + Creates the bucket and uploads the files used by the tests to it + """ + if not file_to_s3_uri_map or not file_to_s3_uri_map.items(): + LOG.debug("No resources to upload") + return + + create_bucket(cls.s3_bucket_name, region=cls.my_region) + + current_file_name = "" + + try: + for file_name, file_info in file_to_s3_uri_map.items(): + current_file_name = file_name + code_path = str(Path(cls.code_dir, file_name)) + LOG.debug("Uploading file %s to bucket %s", file_name, cls.s3_bucket_name) + s3_client = cls.client_provider.s3_client + s3_client.upload_file(code_path, cls.s3_bucket_name, file_name) + LOG.debug("File %s uploaded successfully to bucket %s", file_name, cls.s3_bucket_name) + file_info["uri"] = cls._get_s3_uri(file_name, file_info["type"]) + except ClientError as error: + LOG.error("Upload of file %s to bucket %s failed", current_file_name, cls.s3_bucket_name, exc_info=error) + cls._clean_bucket() + raise error + + @classmethod + def _get_s3_uri(cls, file_name, uri_type): + if uri_type == "s3": + return "s3://{}/{}".format(cls.s3_bucket_name, file_name) + + if cls.my_region == "us-east-1": + return "https://s3.amazonaws.com/{}/{}".format(cls.s3_bucket_name, file_name) + if cls.my_region == "us-iso-east-1": + return "https://s3.us-iso-east-1.c2s.ic.gov/{}/{}".format(cls.s3_bucket_name, file_name) + if cls.my_region == "us-isob-east-1": + return "https://s3.us-isob-east-1.sc2s.sgov.gov/{}/{}".format(cls.s3_bucket_name, file_name) + + return "https://s3-{}.amazonaws.com/{}/{}".format(cls.my_region, cls.s3_bucket_name, file_name) + + def setUp(self): + self.deployer = Deployer(self.client_provider.cfn_client) + + def tearDown(self): + self.client_provider.cfn_client.delete_stack(StackName=self.stack_name) + if os.path.exists(self.output_file_path): + os.remove(self.output_file_path) + if os.path.exists(self.sub_input_file_path): + os.remove(self.sub_input_file_path) + + def create_and_verify_stack(self, file_name, parameters=None): + """ + Creates the Cloud Formation stack and verifies it against the expected + result + + Parameters + ---------- + file_name : string + Template file name + parameters : list + List of parameters + """ + self.output_file_path = str(Path(self.output_dir, "cfn_" + file_name + ".yaml")) + self.expected_resource_path = str(Path(self.expected_dir, file_name + ".json")) + self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + "-" + generate_suffix() + + self._fill_template(file_name) + self.transform_template() + self.deploy_stack(parameters) + self.verify_stack() + + def transform_template(self): + transform_template(self.sub_input_file_path, self.output_file_path) + + def get_region(self): + return self.my_region + + def get_s3_uri(self, file_name): + """ + Returns the S3 URI of a resource file + + Parameters + ---------- + file_name : string + Resource file name + """ + return self.file_to_s3_uri_map[file_name]["uri"] + + def get_code_key_s3_uri(self, code_key): + """ + Returns the S3 URI of a code key for template replacement + + Parameters + ---------- + code_key : string + Template code key + """ + return self.file_to_s3_uri_map[self.code_key_to_file[code_key]]["uri"] + + def get_stack_resources(self, resource_type, stack_resources=None): + if not stack_resources: + stack_resources = self.stack_resources + + resources = [] + for res in stack_resources["StackResourceSummaries"]: + if res["ResourceType"] == resource_type: + resources.append(res) + + return resources + + def get_stack_output(self, output_key): + for output in self.stack_description["Stacks"][0]["Outputs"]: + if output["OutputKey"] == output_key: + return output + return None + + def get_stack_tags(self, output_name): + resource_arn = self.get_stack_output(output_name)["OutputValue"] + return self.client_provider.sfn_client.list_tags_for_resource(resourceArn=resource_arn)["tags"] + + def get_stack_deployment_ids(self): + resources = self.get_stack_resources("AWS::ApiGateway::Deployment") + ids = [] + for res in resources: + ids.append(res["LogicalResourceId"]) + + return ids + + def get_api_stack_stages(self): + resources = self.get_stack_resources("AWS::ApiGateway::RestApi") + + if not resources: + return [] + + return self.client_provider.api_client.get_stages(restApiId=resources[0]["PhysicalResourceId"])["item"] + + def get_api_v2_stack_stages(self): + resources = self.get_stack_resources("AWS::ApiGatewayV2::Api") + + if not resources: + return [] + + return self.client_provider.api_v2_client.get_stages(ApiId=resources[0]["PhysicalResourceId"])["Items"] + + def get_api_v2_endpoint(self, logical_id): + api_id = self.get_physical_id_by_logical_id(logical_id) + api = self.client_provider.api_v2_client.get_api(ApiId=api_id) + return api["ApiEndpoint"] + + def get_stack_nested_stack_resources(self): + resources = self.get_stack_resources("AWS::CloudFormation::Stack") + + if not resources: + return None + + return self.client_provider.cfn_client.list_stack_resources(StackName=resources[0]["PhysicalResourceId"]) + + def get_stack_outputs(self): + if not self.stack_description: + return {} + output_list = self.stack_description["Stacks"][0]["Outputs"] + return {output["OutputKey"]: output["OutputValue"] for output in output_list} + + def get_resource_status_by_logical_id(self, logical_id): + if not self.stack_resources: + return None + + for res in self.stack_resources["StackResourceSummaries"]: + if res["LogicalResourceId"] == logical_id: + return res["ResourceStatus"] + + return None + + def get_physical_id_by_type(self, resource_type): + if not self.stack_resources: + return None + + for res in self.stack_resources["StackResourceSummaries"]: + if res["ResourceType"] == resource_type: + return res["PhysicalResourceId"] + + return None + + def get_logical_id_by_type(self, resource_type): + if not self.stack_resources: + return None + + for res in self.stack_resources["StackResourceSummaries"]: + if res["ResourceType"] == resource_type: + return res["LogicalResourceId"] + + return None + + def get_physical_id_by_logical_id(self, logical_id): + if not self.stack_resources: + return None + + for res in self.stack_resources["StackResourceSummaries"]: + if res["LogicalResourceId"] == logical_id: + return res["PhysicalResourceId"] + + return None + + def _fill_template(self, file_name): + """ + Replaces the template variables with their value + + Parameters + ---------- + file_name : string + Template file name + """ + input_file_path = str(Path(self.template_dir, file_name + ".yaml")) + updated_template_path = str(Path(self.output_dir, "sub_" + file_name + ".yaml")) + with open(input_file_path) as f: + data = f.read() + for key, _ in self.code_key_to_file.items(): + # We must double the {} to escape them so they will survive a round of unescape + data = data.replace("${{{}}}".format(key), self.get_code_key_s3_uri(key)) + yaml_doc = yaml.load(data, Loader=yaml.FullLoader) + + dump_yaml(updated_template_path, yaml_doc) + + self.sub_input_file_path = updated_template_path + + def set_template_resource_property(self, resource_name, property_name, value): + """ + Updates a resource property of the current SAM template + + Parameters + ---------- + resource_name: string + resource name + property_name: string + property name + value + value + """ + yaml_doc = load_yaml(self.sub_input_file_path) + yaml_doc["Resources"][resource_name]["Properties"][property_name] = value + dump_yaml(self.sub_input_file_path, yaml_doc) + + def get_template_resource_property(self, resource_name, property_name): + yaml_doc = load_yaml(self.sub_input_file_path) + return yaml_doc["Resources"][resource_name]["Properties"][property_name] + + def deploy_stack(self, parameters=None): + """ + Deploys the current cloud formation stack + """ + with open(self.output_file_path) as cfn_file: + result, changeset_type = self.deployer.create_and_wait_for_changeset( + stack_name=self.stack_name, + cfn_template=cfn_file.read(), + parameter_values=[] if parameters is None else parameters, + capabilities=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], + role_arn=None, + notification_arns=[], + s3_uploader=None, + tags=[], + ) + self.deployer.execute_changeset(result["Id"], self.stack_name) + self.deployer.wait_for_execute(self.stack_name, changeset_type) + + self.stack_description = self.client_provider.cfn_client.describe_stacks(StackName=self.stack_name) + self.stack_resources = self.client_provider.cfn_client.list_stack_resources(StackName=self.stack_name) + + def verify_stack(self): + """ + Gets and compares the Cloud Formation stack against the expect result file + """ + # verify if the stack was successfully created + self.assertEqual(self.stack_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE") + # verify if the stack contains the expected resources + error = verify_stack_resources(self.expected_resource_path, self.stack_resources) + if error: + self.fail(error) diff --git a/integration/helpers/client_provider.py b/integration/helpers/client_provider.py new file mode 100644 index 0000000000..2ffab0e19d --- /dev/null +++ b/integration/helpers/client_provider.py @@ -0,0 +1,77 @@ +import boto3 +from botocore.config import Config + + +class ClientProvider: + def __init__(self): + self._cloudformation_client = None + self._s3_client = None + self._api_client = None + self._lambda_client = None + self._iam_client = None + self._api_v2_client = None + self._sfn_client = None + + @property + def cfn_client(self): + """ + Cloudformation Client + """ + if not self._cloudformation_client: + config = Config(retries={"max_attempts": 10, "mode": "standard"}) + self._cloudformation_client = boto3.client("cloudformation", config=config) + return self._cloudformation_client + + @property + def s3_client(self): + """ + S3 Client + """ + if not self._s3_client: + self._s3_client = boto3.client("s3") + return self._s3_client + + @property + def api_client(self): + """ + APIGateway Client + """ + if not self._api_client: + self._api_client = boto3.client("apigateway") + return self._api_client + + @property + def lambda_client(self): + """ + Lambda Client + """ + if not self._lambda_client: + self._lambda_client = boto3.client("lambda") + return self._lambda_client + + @property + def iam_client(self): + """ + IAM Client + """ + if not self._iam_client: + self._iam_client = boto3.client("iam") + return self._iam_client + + @property + def api_v2_client(self): + """ + APIGatewayV2 Client + """ + if not self._api_v2_client: + self._api_v2_client = boto3.client("apigatewayv2") + return self._api_v2_client + + @property + def sfn_client(self): + """ + Step Functions Client + """ + if not self._sfn_client: + self._sfn_client = boto3.client("stepfunctions") + return self._sfn_client diff --git a/integration/helpers/deployer/__init__.py b/integration/helpers/deployer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/helpers/deployer/deployer.py b/integration/helpers/deployer/deployer.py new file mode 100644 index 0000000000..4cb0de31f6 --- /dev/null +++ b/integration/helpers/deployer/deployer.py @@ -0,0 +1,494 @@ +""" +Cloudformation deploy class which also streams events and changeset information +This was ported over from the sam-cli repo +""" + +# Copyright 2012-2015 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. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +# This is a modified version of the Deployer class from aws-sam-cli +# (and its dependencies) to work with python 2 +# Modifications: +# - Imports now reference local classes +# - Alternative imports for python2 +# - py3 -> py2 migrations (ex: "".format() instead of f"", no "from" for raise) +# - Moved UserException to exceptions.py +# - Moved DeployColor to colors.py +# - Removed unnecessary functions from artifact_exporter +import sys +import math +from collections import OrderedDict +import logging +import time +from datetime import datetime + +import botocore + +from integration.helpers.deployer.utils.colors import DeployColor +from integration.helpers.deployer.exceptions import exceptions as deploy_exceptions +from integration.helpers.deployer.utils.table_print import ( + pprint_column_names, + pprint_columns, + newline_per_item, + MIN_OFFSET, +) +from integration.helpers.deployer.utils.artifact_exporter import mktempfile, parse_s3_url +from integration.helpers.deployer.utils.time import utc_to_timestamp + +LOG = logging.getLogger(__name__) + +DESCRIBE_STACK_EVENTS_FORMAT_STRING = ( + "{ResourceStatus:<{0}} {ResourceType:<{1}} {LogicalResourceId:<{2}} {ResourceStatusReason:<{3}}" +) +DESCRIBE_STACK_EVENTS_DEFAULT_ARGS = OrderedDict( + { + "ResourceStatus": "ResourceStatus", + "ResourceType": "ResourceType", + "LogicalResourceId": "LogicalResourceId", + "ResourceStatusReason": "ResourceStatusReason", + } +) + +DESCRIBE_STACK_EVENTS_TABLE_HEADER_NAME = "CloudFormation events from changeset" + +DESCRIBE_CHANGESET_FORMAT_STRING = "{Operation:<{0}} {LogicalResourceId:<{1}} {ResourceType:<{2}} {Replacement:<{3}}" +DESCRIBE_CHANGESET_DEFAULT_ARGS = OrderedDict( + { + "Operation": "Operation", + "LogicalResourceId": "LogicalResourceId", + "ResourceType": "ResourceType", + "Replacement": "Replacement", + } +) + +DESCRIBE_CHANGESET_TABLE_HEADER_NAME = "CloudFormation stack changeset" + +OUTPUTS_FORMAT_STRING = "{Outputs:<{0}}" +OUTPUTS_DEFAULTS_ARGS = OrderedDict({"Outputs": "Outputs"}) + +OUTPUTS_TABLE_HEADER_NAME = "CloudFormation outputs from deployed stack" + + +class Deployer: + def __init__(self, cloudformation_client, changeset_prefix="sam-integ-"): + self._client = cloudformation_client + self.changeset_prefix = changeset_prefix + # 500ms of sleep time between stack checks and describe stack events. + self.client_sleep = 0.5 + # 2000ms of backoff time which is exponentially used, when there are exceptions during describe stack events + self.backoff = 2 + # Maximum number of attempts before raising exception back up the chain. + self.max_attempts = 3 + self.deploy_color = DeployColor() + + def has_stack(self, stack_name): + """ + Checks if a CloudFormation stack with given name exists + + :param stack_name: Name or ID of the stack + :return: True if stack exists. False otherwise + """ + try: + resp = self._client.describe_stacks(StackName=stack_name) + if not resp["Stacks"]: + return False + + # When you run CreateChangeSet on a a stack that does not exist, + # CloudFormation will create a stack and set it's status + # REVIEW_IN_PROGRESS. However this stack is cannot be manipulated + # by "update" commands. Under this circumstances, we treat like + # this stack does not exist and call CreateChangeSet will + # ChangeSetType set to CREATE and not UPDATE. + stack = resp["Stacks"][0] + return stack["StackStatus"] != "REVIEW_IN_PROGRESS" + + except botocore.exceptions.ClientError as e: + # If a stack does not exist, describe_stacks will throw an + # exception. Unfortunately we don't have a better way than parsing + # the exception msg to understand the nature of this exception. + + if "Stack with id {0} does not exist".format(stack_name) in str(e): + LOG.debug("Stack with id %s does not exist", stack_name) + return False + except botocore.exceptions.BotoCoreError as e: + # If there are credentials, environment errors, + # catch that and throw a deploy failed error. + + LOG.debug("Botocore Exception : %s", str(e)) + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(e)) + + except Exception as e: + # We don't know anything about this exception. Don't handle + LOG.debug("Unable to get stack details.", exc_info=e) + raise e + + def create_changeset( + self, stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags + ): + """ + Call Cloudformation to create a changeset and wait for it to complete + + :param stack_name: Name or ID of stack + :param cfn_template: CloudFormation template string + :param parameter_values: Template parameters object + :param capabilities: Array of capabilities passed to CloudFormation + :param tags: Array of tags passed to CloudFormation + :return: + """ + if not self.has_stack(stack_name): + changeset_type = "CREATE" + # When creating a new stack, UsePreviousValue=True is invalid. + # For such parameters, users should either override with new value, + # or set a Default value in template to successfully create a stack. + parameter_values = [x for x in parameter_values if not x.get("UsePreviousValue", False)] + else: + changeset_type = "UPDATE" + # UsePreviousValue not valid if parameter is new + summary = self._client.get_template_summary(StackName=stack_name) + existing_parameters = [parameter["ParameterKey"] for parameter in summary["Parameters"]] + parameter_values = [ + x + for x in parameter_values + if not (x.get("UsePreviousValue", False) and x["ParameterKey"] not in existing_parameters) + ] + + # Each changeset will get a unique name based on time. + # Description is also setup based on current date and that SAM CLI is used. + kwargs = { + "ChangeSetName": self.changeset_prefix + str(int(time.time())), + "StackName": stack_name, + "TemplateBody": cfn_template, + "ChangeSetType": changeset_type, + "Parameters": parameter_values, + "Capabilities": capabilities, + "Description": "Created by SAM CLI at {0} UTC".format(datetime.utcnow().isoformat()), + "Tags": tags, + } + + # If an S3 uploader is available, use TemplateURL to deploy rather than + # TemplateBody. This is required for large templates. + if s3_uploader: + with mktempfile() as temporary_file: + temporary_file.write(kwargs.pop("TemplateBody")) + temporary_file.flush() + + # TemplateUrl property requires S3 URL to be in path-style format + parts = parse_s3_url( + s3_uploader.upload_with_dedup(temporary_file.name, "template"), version_property="Version" + ) + kwargs["TemplateURL"] = s3_uploader.to_path_style_s3_url(parts["Key"], parts.get("Version", None)) + + # don't set these arguments if not specified to use existing values + if role_arn is not None: + kwargs["RoleARN"] = role_arn + if notification_arns is not None: + kwargs["NotificationARNs"] = notification_arns + return self._create_change_set(stack_name=stack_name, changeset_type=changeset_type, **kwargs) + + def _create_change_set(self, stack_name, changeset_type, **kwargs): + try: + resp = self._client.create_change_set(**kwargs) + return resp, changeset_type + except botocore.exceptions.ClientError as ex: + if "The bucket you are attempting to access must be addressed using the specified endpoint" in str(ex): + raise deploy_exceptions.DeployBucketInDifferentRegionError( + "Failed to create/update stack {}".format(stack_name) + ) + raise deploy_exceptions.ChangeSetError(stack_name=stack_name, msg=str(ex)) + + except Exception as ex: + LOG.debug("Unable to create changeset", exc_info=ex) + raise deploy_exceptions.ChangeSetError(stack_name=stack_name, msg=str(ex)) + + @pprint_column_names( + format_string=DESCRIBE_CHANGESET_FORMAT_STRING, + format_kwargs=DESCRIBE_CHANGESET_DEFAULT_ARGS, + table_header=DESCRIBE_CHANGESET_TABLE_HEADER_NAME, + ) + def describe_changeset(self, change_set_id, stack_name, **kwargs): + """ + Call Cloudformation to describe a changeset + + :param change_set_id: ID of the changeset + :param stack_name: Name of the CloudFormation stack + :return: dictionary of changes described in the changeset. + """ + paginator = self._client.get_paginator("describe_change_set") + response_iterator = paginator.paginate(ChangeSetName=change_set_id, StackName=stack_name) + changes = {"Add": [], "Modify": [], "Remove": []} + changes_showcase = {"Add": "+ Add", "Modify": "* Modify", "Remove": "- Delete"} + changeset = False + for item in response_iterator: + cf_changes = item.get("Changes") + for change in cf_changes: + changeset = True + resource_props = change.get("ResourceChange") + action = resource_props.get("Action") + changes[action].append( + { + "LogicalResourceId": resource_props.get("LogicalResourceId"), + "ResourceType": resource_props.get("ResourceType"), + "Replacement": "N/A" + if resource_props.get("Replacement") is None + else resource_props.get("Replacement"), + } + ) + + for k, v in changes.items(): + for value in v: + row_color = self.deploy_color.get_changeset_action_color(action=k) + pprint_columns( + columns=[ + changes_showcase.get(k, k), + value["LogicalResourceId"], + value["ResourceType"], + value["Replacement"], + ], + width=kwargs["width"], + margin=kwargs["margin"], + format_string=DESCRIBE_CHANGESET_FORMAT_STRING, + format_args=kwargs["format_args"], + columns_dict=DESCRIBE_CHANGESET_DEFAULT_ARGS.copy(), + color=row_color, + ) + + if not changeset: + # There can be cases where there are no changes, + # but could be an an addition of a SNS notification topic. + pprint_columns( + columns=["-", "-", "-", "-"], + width=kwargs["width"], + margin=kwargs["margin"], + format_string=DESCRIBE_CHANGESET_FORMAT_STRING, + format_args=kwargs["format_args"], + columns_dict=DESCRIBE_CHANGESET_DEFAULT_ARGS.copy(), + ) + + return changes + + def wait_for_changeset(self, changeset_id, stack_name): + """ + Waits until the changeset creation completes + + :param changeset_id: ID or name of the changeset + :param stack_name: Stack name + :return: Latest status of the create-change-set operation + """ + sys.stdout.write("\nWaiting for changeset to be created..\n") + sys.stdout.flush() + + # Wait for changeset to be created + waiter = self._client.get_waiter("change_set_create_complete") + # Poll every 5 seconds. Changeset creation should be fast + waiter_config = {"Delay": 5} + try: + waiter.wait(ChangeSetName=changeset_id, StackName=stack_name, WaiterConfig=waiter_config) + except botocore.exceptions.WaiterError as ex: + + resp = ex.last_response + status = resp["Status"] + reason = resp["StatusReason"] + + if ( + status == "FAILED" + and "The submitted information didn't contain changes." in reason + or "No updates are to be performed" in reason + ): + raise deploy_exceptions.ChangeEmptyError(stack_name=stack_name) + + raise deploy_exceptions.ChangeSetError( + stack_name=stack_name, msg="ex: {0} Status: {1}. Reason: {2}".format(ex, status, reason) + ) + + def execute_changeset(self, changeset_id, stack_name): + """ + Calls CloudFormation to execute changeset + + :param changeset_id: ID of the changeset + :param stack_name: Name or ID of the stack + :return: Response from execute-change-set call + """ + try: + return self._client.execute_change_set(ChangeSetName=changeset_id, StackName=stack_name) + except botocore.exceptions.ClientError as ex: + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) + + def get_last_event_time(self, stack_name): + """ + Finds the last event time stamp thats present for the stack, if not get the current time + :param stack_name: Name or ID of the stack + :return: unix epoch + """ + try: + return utc_to_timestamp( + self._client.describe_stack_events(StackName=stack_name)["StackEvents"][0]["Timestamp"] + ) + except KeyError: + return time.time() + + @pprint_column_names( + format_string=DESCRIBE_STACK_EVENTS_FORMAT_STRING, + format_kwargs=DESCRIBE_STACK_EVENTS_DEFAULT_ARGS, + table_header=DESCRIBE_STACK_EVENTS_TABLE_HEADER_NAME, + ) + def describe_stack_events(self, stack_name, time_stamp_marker, **kwargs): + """ + Calls CloudFormation to get current stack events + :param stack_name: Name or ID of the stack + :param time_stamp_marker: last event time on the stack to start streaming events from. + :return: + """ + + stack_change_in_progress = True + events = set() + retry_attempts = 0 + + while stack_change_in_progress and retry_attempts <= self.max_attempts: + try: + + # Only sleep if there have been no retry_attempts + time.sleep(self.client_sleep if retry_attempts == 0 else 0) + describe_stacks_resp = self._client.describe_stacks(StackName=stack_name) + paginator = self._client.get_paginator("describe_stack_events") + response_iterator = paginator.paginate(StackName=stack_name) + stack_status = describe_stacks_resp["Stacks"][0]["StackStatus"] + latest_time_stamp_marker = time_stamp_marker + for event_items in response_iterator: + for event in event_items["StackEvents"]: + if event["EventId"] not in events and utc_to_timestamp(event["Timestamp"]) > time_stamp_marker: + events.add(event["EventId"]) + latest_time_stamp_marker = max( + latest_time_stamp_marker, utc_to_timestamp(event["Timestamp"]) + ) + row_color = self.deploy_color.get_stack_events_status_color(status=event["ResourceStatus"]) + pprint_columns( + columns=[ + event["ResourceStatus"], + event["ResourceType"], + event["LogicalResourceId"], + event.get("ResourceStatusReason", "-"), + ], + width=kwargs["width"], + margin=kwargs["margin"], + format_string=DESCRIBE_STACK_EVENTS_FORMAT_STRING, + format_args=kwargs["format_args"], + columns_dict=DESCRIBE_STACK_EVENTS_DEFAULT_ARGS.copy(), + color=row_color, + ) + # Skip already shown old event entries + elif utc_to_timestamp(event["Timestamp"]) <= time_stamp_marker: + time_stamp_marker = latest_time_stamp_marker + break + else: # go to next loop if not break from inside loop + time_stamp_marker = latest_time_stamp_marker # update marker if all events are new + continue + break # reached here only if break from inner loop! + + if self._check_stack_complete(stack_status): + stack_change_in_progress = False + break + except botocore.exceptions.ClientError as ex: + retry_attempts = retry_attempts + 1 + if retry_attempts > self.max_attempts: + LOG.error("Describing stack events for %s failed: %s", stack_name, str(ex)) + return + # Sleep in exponential backoff mode + time.sleep(math.pow(self.backoff, retry_attempts)) + + def _check_stack_complete(self, status): + return "COMPLETE" in status and "CLEANUP" not in status + + def wait_for_execute(self, stack_name, changeset_type): + sys.stdout.write( + "\n{} - Waiting for stack create/update " + "to complete\n".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + ) + sys.stdout.flush() + + self.describe_stack_events(stack_name, self.get_last_event_time(stack_name)) + + # Pick the right waiter + if changeset_type == "CREATE": + waiter = self._client.get_waiter("stack_create_complete") + elif changeset_type == "UPDATE": + waiter = self._client.get_waiter("stack_update_complete") + else: + raise RuntimeError("Invalid changeset type {0}".format(changeset_type)) + + # Poll every 30 seconds. Polling too frequently risks hitting rate limits + # on CloudFormation's DescribeStacks API + waiter_config = {"Delay": 30, "MaxAttempts": 120} + + try: + waiter.wait(StackName=stack_name, WaiterConfig=waiter_config) + except botocore.exceptions.WaiterError as ex: + LOG.debug("Execute changeset waiter exception", exc_info=ex) + + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) + + outputs = self.get_stack_outputs(stack_name=stack_name, echo=False) + if outputs: + self._display_stack_outputs(outputs) + + def create_and_wait_for_changeset( + self, stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags + ): + try: + result, changeset_type = self.create_changeset( + stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags + ) + self.wait_for_changeset(result["Id"], stack_name) + self.describe_changeset(result["Id"], stack_name) + return result, changeset_type + except botocore.exceptions.ClientError as ex: + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) + + @pprint_column_names( + format_string=OUTPUTS_FORMAT_STRING, format_kwargs=OUTPUTS_DEFAULTS_ARGS, table_header=OUTPUTS_TABLE_HEADER_NAME + ) + def _display_stack_outputs(self, stack_outputs, **kwargs): + for counter, output in enumerate(stack_outputs): + for k, v in [ + ("Key", output.get("OutputKey")), + ("Description", output.get("Description", "-")), + ("Value", output.get("OutputValue")), + ]: + pprint_columns( + columns=["{k:<{0}}{v:<{0}}".format(MIN_OFFSET, k=k, v=v)], + width=kwargs["width"], + margin=kwargs["margin"], + format_string=OUTPUTS_FORMAT_STRING, + format_args=kwargs["format_args"], + columns_dict=OUTPUTS_DEFAULTS_ARGS.copy(), + color="green", + replace_whitespace=False, + break_long_words=False, + drop_whitespace=False, + ) + newline_per_item(stack_outputs, counter) + + def get_stack_outputs(self, stack_name, echo=True): + try: + stacks_description = self._client.describe_stacks(StackName=stack_name) + try: + outputs = stacks_description["Stacks"][0]["Outputs"] + if echo: + sys.stdout.write("\nStack {stack_name} outputs:\n".format(stack_name=stack_name)) + sys.stdout.flush() + self._display_stack_outputs(stack_outputs=outputs) + return outputs + except KeyError: + return None + + except botocore.exceptions.ClientError as ex: + raise deploy_exceptions.DeployStackOutPutFailedError(stack_name=stack_name, msg=str(ex)) diff --git a/integration/helpers/deployer/exceptions/__init__.py b/integration/helpers/deployer/exceptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/helpers/deployer/exceptions/exceptions.py b/integration/helpers/deployer/exceptions/exceptions.py new file mode 100644 index 0000000000..3dee92caa3 --- /dev/null +++ b/integration/helpers/deployer/exceptions/exceptions.py @@ -0,0 +1,65 @@ +""" +Exceptions that are raised by sam deploy +This was ported over from the sam-cli repo +""" +import click + + +class UserException(click.ClickException): + """ + Base class for all exceptions that need to be surfaced to the user. Typically, we will display the exception + message to user and return the error code from CLI process + """ + + exit_code = 1 + + def __init__(self, message, wrapped_from=None): + self.wrapped_from = wrapped_from + + click.ClickException.__init__(self, message) + + +class ChangeEmptyError(UserException): + def __init__(self, stack_name): + self.stack_name = stack_name + message_fmt = "No changes to deploy. Stack {stack_name} is up to date" + super(ChangeEmptyError, self).__init__(message=message_fmt.format(stack_name=self.stack_name)) + + +class ChangeSetError(UserException): + def __init__(self, stack_name, msg): + self.stack_name = stack_name + self.msg = msg + message_fmt = "Failed to create changeset for the stack: {stack_name}, {msg}" + super(ChangeSetError, self).__init__(message=message_fmt.format(stack_name=self.stack_name, msg=self.msg)) + + +class DeployFailedError(UserException): + def __init__(self, stack_name, msg): + self.stack_name = stack_name + self.msg = msg + + message_fmt = "Failed to create/update the stack: {stack_name}, {msg}" + + super(DeployFailedError, self).__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg)) + + +class DeployStackOutPutFailedError(UserException): + def __init__(self, stack_name, msg): + self.stack_name = stack_name + self.msg = msg + + message_fmt = "Failed to get outputs from stack: {stack_name}, {msg}" + + super(DeployStackOutPutFailedError, self).__init__( + message=message_fmt.format(stack_name=self.stack_name, msg=msg) + ) + + +class DeployBucketInDifferentRegionError(UserException): + def __init__(self, msg): + self.msg = msg + + message_fmt = "{msg} : deployment s3 bucket is in a different region, try sam deploy --guided" + + super(DeployBucketInDifferentRegionError, self).__init__(message=message_fmt.format(msg=self.msg)) diff --git a/integration/helpers/deployer/utils/__init__.py b/integration/helpers/deployer/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/helpers/deployer/utils/artifact_exporter.py b/integration/helpers/deployer/utils/artifact_exporter.py new file mode 100644 index 0000000000..6a06dfdbbb --- /dev/null +++ b/integration/helpers/deployer/utils/artifact_exporter.py @@ -0,0 +1,64 @@ +""" +Logic for uploading to S3 per Cloudformation Specific Resource +This was ported over from the sam-cli repo +""" +# pylint: disable=no-member + +# Copyright 2012-2015 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. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import os +import tempfile +import contextlib +from contextlib import contextmanager + +try: + from urllib.parse import urlparse, parse_qs +except ImportError: # py2 + from urlparse import urlparse, parse_qs +import uuid + + +def parse_s3_url(url, bucket_name_property="Bucket", object_key_property="Key", version_property=None): + + if isinstance(url, str) and url.startswith("s3://"): + + parsed = urlparse(url) + query = parse_qs(parsed.query) + + if parsed.netloc and parsed.path: + result = dict() + result[bucket_name_property] = parsed.netloc + result[object_key_property] = parsed.path.lstrip("/") + + # If there is a query string that has a single versionId field, + # set the object version and return + if version_property is not None and "versionId" in query and len(query["versionId"]) == 1: + result[version_property] = query["versionId"][0] + + return result + + raise ValueError("URL given to the parse method is not a valid S3 url " "{0}".format(url)) + + +@contextmanager +def mktempfile(): + directory = tempfile.gettempdir() + filename = os.path.join(directory, uuid.uuid4().hex) + + try: + with open(filename, "w+") as handle: + yield handle + finally: + if os.path.exists(filename): + os.remove(filename) diff --git a/integration/helpers/deployer/utils/colors.py b/integration/helpers/deployer/utils/colors.py new file mode 100644 index 0000000000..a653e22533 --- /dev/null +++ b/integration/helpers/deployer/utils/colors.py @@ -0,0 +1,94 @@ +""" +Wrapper to generated colored messages for printing in Terminal +This was ported over from the sam-cli repo +""" + +import click + + +class Colored: + """ + Helper class to add ANSI colors and decorations to text. Given a string, ANSI colors are added with special prefix + and suffix characters that are specially interpreted by Terminals to display colors. + + Ex: "message" -> add red color -> \x1b[31mmessage\x1b[0m + + This class serves two purposes: + - Hide the underlying library used to provide colors: In this case, we use ``click`` library which is usually + used to build a CLI interface. We use ``click`` just to minimize the number of dependencies we add to this + project. This class allows us to replace click with any other color library like ``pygments`` without + changing callers. + + - Transparently turn off colors: In cases when the string is not written to Terminal (ex: log file) the ANSI + color codes should not be written. This class supports the scenario by allowing you to turn off colors. + Calls to methods like `red()` will simply return the input string. + """ + + def __init__(self, colorize=True): + """ + Initialize the object + + Parameters + ---------- + colorize : bool + Optional. Set this to True to turn on coloring. False will turn off coloring + """ + self.colorize = colorize + + def red(self, msg): + """Color the input red""" + return self._color(msg, "red") + + def green(self, msg): + """Color the input green""" + return self._color(msg, "green") + + def cyan(self, msg): + """Color the input cyan""" + return self._color(msg, "cyan") + + def white(self, msg): + """Color the input white""" + return self._color(msg, "white") + + def yellow(self, msg): + """Color the input yellow""" + return self._color(msg, "yellow") + + def underline(self, msg): + """Underline the input""" + return click.style(msg, underline=True) if self.colorize else msg + + def _color(self, msg, color): + """Internal helper method to add colors to input""" + kwargs = {"fg": color} + return click.style(msg, **kwargs) if self.colorize else msg + + +class DeployColor: + def __init__(self): + self._color = Colored() + self.changeset_color_map = {"Add": "green", "Modify": "yellow", "Remove": "red"} + self.status_color_map = { + "CREATE_COMPLETE": "green", + "CREATE_FAILED": "red", + "CREATE_IN_PROGRESS": "yellow", + "DELETE_COMPLETE": "green", + "DELETE_FAILED": "red", + "DELETE_IN_PROGRESS": "red", + "REVIEW_IN_PROGRESS": "yellow", + "ROLLBACK_COMPLETE": "red", + "ROLLBACK_IN_PROGRESS": "red", + "UPDATE_COMPLETE": "green", + "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS": "yellow", + "UPDATE_IN_PROGRESS": "yellow", + "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS": "red", + "UPDATE_ROLLBACK_FAILED": "red", + "UPDATE_ROLLBACK_IN_PROGRESS": "red", + } + + def get_stack_events_status_color(self, status): + return self.status_color_map.get(status, "yellow") + + def get_changeset_action_color(self, action): + return self.changeset_color_map.get(action, "yellow") diff --git a/integration/helpers/deployer/utils/table_print.py b/integration/helpers/deployer/utils/table_print.py new file mode 100644 index 0000000000..4cc27d6f30 --- /dev/null +++ b/integration/helpers/deployer/utils/table_print.py @@ -0,0 +1,168 @@ +""" +Utilities for table pretty printing using click +This was ported over from the sam-cli repo +""" +from itertools import count + +try: + from itertools import zip_longest +except ImportError: # py2 + from itertools import izip_longest as zip_longest +import textwrap +from functools import wraps + +import click + +MIN_OFFSET = 20 + + +def pprint_column_names(format_string, format_kwargs, margin=None, table_header=None, color="yellow"): + """ + Prints column names + + Parameters + ---------- + format_string : str + format string to be used that has the strings, minimum width to be replaced + format_kwargs : list + dictionary that is supplied to the format_string to format the string + margin : int, optional + margin that is to be reduced from column width for columnar text, by default None + table_header : str, optional + Text to display before the table, by default None + color : str, optional + Table color, by default "yellow" + + Returns + ------- + str + Complete table string representation + + Raises + ------ + ValueError + format_kwargs is empty + ValueError + [description] + """ + min_width = 100 + min_margin = 2 + + def pprint_wrap(func): + # Calculate terminal width, number of columns in the table + width, _ = click.get_terminal_size() + # For UX purposes, set a minimum width for the table to be usable + # and usable_width keeps margins in mind. + width = max(width, min_width) + + total_args = len(format_kwargs) + if not total_args: + raise ValueError("Number of arguments supplied should be > 0 , format_kwargs: {}".format(format_kwargs)) + + # Get width to be a usable number so that we can equally divide the space for all the columns. + # Can be refactored, to allow for modularity in the shaping of the columns. + width = width - (width % total_args) + usable_width_no_margin = int(width) - 1 + usable_width = int((usable_width_no_margin - (margin if margin else min_margin))) + if total_args > int(usable_width / 2): + raise ValueError("Total number of columns exceed available width") + width_per_column = int(usable_width / total_args) + + # The final column should not roll over into the next line + final_arg_width = width_per_column - 1 + + # the format string contains minimumwidth that need to be set. + # eg: "{a:{0}}} {b:<{1}}} {c:{2}}}" + format_args = [width_per_column for _ in range(total_args - 1)] + format_args.extend([final_arg_width]) + + # format arguments are now ready for setting minimumwidth + + @wraps(func) + def wrap(*args, **kwargs): + # The table is setup with the column names, format_string contains the column names. + if table_header: + click.secho("\n" + table_header) + click.secho("-" * usable_width, fg=color) + click.secho(format_string.format(*format_args, **format_kwargs), fg=color) + click.secho("-" * usable_width, fg=color) + # format_args which have the minimumwidth set per {} in the format_string is passed to the function + # which this decorator wraps, so that the function has access to the correct format_args + kwargs["format_args"] = format_args + kwargs["width"] = width_per_column + kwargs["margin"] = margin if margin else min_margin + result = func(*args, **kwargs) + # Complete the table + click.secho("-" * usable_width, fg=color) + return result + + return wrap + + return pprint_wrap + + +def wrapped_text_generator(texts, width, margin, **textwrap_kwargs): + """ + Returns a generator where the contents are wrapped text to a specified width + + Parameters + ---------- + texts : list + list of text that needs to be wrapped at specified width + width : int + width of the text to be wrapped + margin : int + margin to be reduced from width for cleaner UX + + Yields + ------- + func + generator of wrapped text + """ + for text in texts: + yield textwrap.wrap(text, width=width - margin, **textwrap_kwargs) + + +def pprint_columns(columns, width, margin, format_string, format_args, columns_dict, color="yellow", **textwrap_kwargs): + """ + Prints columns based on list of columnar text, associated formatting string and associated format arguments. + + Parameters + ---------- + columns : list + List of columnnar text that go into columns as specified by the format_string + width : int + Width of the text to be wrapped + margin : int + Margin to be reduced from width for cleaner UX + format_string : str + Format string that has both width and text specifiers set. + format_args : list + List of offset specifiers + columns_dict : dict + Arguments dictionary that have dummy values per column + color : str, optional + Rows color, by default "yellow" + """ + for columns_text in zip_longest(*wrapped_text_generator(columns, width, margin, **textwrap_kwargs), fillvalue=""): + counter = count() + # Generate columnar data that correspond to the column names and update them. + for k, _ in columns_dict.items(): + columns_dict[k] = columns_text[next(counter)] + + click.secho(format_string.format(*format_args, **columns_dict), fg=color) + + +def newline_per_item(iterable, counter): + """ + Adds a new line based on the index of a given iterable + + Parameters + ---------- + iterable : iterable + Any iterable that implements __len__ + counter : int + Current index within the iterable + """ + if counter < len(iterable) - 1: + click.echo(message="", nl=True) diff --git a/integration/helpers/deployer/utils/time.py b/integration/helpers/deployer/utils/time.py new file mode 100644 index 0000000000..6a5706aaef --- /dev/null +++ b/integration/helpers/deployer/utils/time.py @@ -0,0 +1,128 @@ +""" +Date & Time related utilities +This was ported over from the sam-cli repo +""" + +import datetime +import dateparser + +from dateutil.tz import tzutc + + +def timestamp_to_iso(timestamp): + """ + Convert Unix Epoch Timestamp to ISO formatted time string: + Ex: 1234567890 -> 2018-07-05T03:09:43.842000 + + Parameters + ---------- + timestamp : int + Unix epoch timestamp + + Returns + ------- + str + ISO formatted time string + """ + + return to_datetime(timestamp).isoformat() + + +def to_datetime(timestamp): + """ + Convert Unix Epoch Timestamp to Python's ``datetime.datetime`` object + + Parameters + ---------- + timestamp : int + Unix epoch timestamp + + Returns + ------- + datetime.datetime + Datetime representation of timestamp + """ + + timestamp_secs = int(timestamp) / 1000.0 + return datetime.datetime.utcfromtimestamp(timestamp_secs) + + +def to_timestamp(some_time): + """ + Converts the given datetime value to Unix timestamp + + Parameters + ---------- + some_time : datetime.datetime + Value to be converted to unix epoch. This must be without any timezone identifier + + Returns + ------- + int + Unix timestamp of the given time + """ + + # `total_seconds()` returns elaped microseconds as a float. Get just milliseconds and discard the rest. + return int((some_time - datetime.datetime(1970, 1, 1)).total_seconds() * 1000.0) + + +def utc_to_timestamp(utc): + """ + Converts utc timestamp with tz_info set to utc to Unix timestamp + :param utc: datetime.datetime + :return: UNIX timestamp + """ + + return to_timestamp(utc.replace(tzinfo=None)) + + +def to_utc(some_time): + """ + Convert the given date to UTC, if the date contains a timezone. + + Parameters + ---------- + some_time : datetime.datetime + datetime object to convert to UTC + + Returns + ------- + datetime.datetime + Converted datetime object + """ + + # Convert timezone aware objects to UTC + if some_time.tzinfo and some_time.utcoffset(): + some_time = some_time.astimezone(tzutc()) + + # Now that time is UTC, simply remove the timezone component. + return some_time.replace(tzinfo=None) + + +def parse_date(date_string): + """ + Parse the given string as datetime object. This parser supports in almost any string formats. + + For relative times, like `10min ago`, this parser computes the actual time relative to current UTC time. This + allows time to always be in UTC if an explicit time zone is not provided. + + Parameters + ---------- + date_string : str + String representing the date + + Returns + ------- + datetime.datetime + Parsed datetime object. None, if the string cannot be parsed. + """ + + parser_settings = { + # Relative times like '10m ago' must subtract from the current UTC time. Without this setting, dateparser + # will use current local time as the base for subtraction, but falsely assume it is a UTC time. Therefore + # the time that dateparser returns will be a `datetime` object that did not have any timezone information. + # So be explicit to set the time to UTC. + "RELATIVE_BASE": datetime.datetime.utcnow() + } + + return dateparser.parse(date_string, settings=parser_settings) diff --git a/integration/helpers/file_resources.py b/integration/helpers/file_resources.py new file mode 100644 index 0000000000..eadd533363 --- /dev/null +++ b/integration/helpers/file_resources.py @@ -0,0 +1,14 @@ +FILE_TO_S3_URI_MAP = { + "code.zip": {"type": "s3", "uri": ""}, + "layer1.zip": {"type": "s3", "uri": ""}, + "swagger1.json": {"type": "s3", "uri": ""}, + "swagger2.json": {"type": "s3", "uri": ""}, + "template.yaml": {"type": "http", "uri": ""}, +} + +CODE_KEY_TO_FILE_MAP = { + "codeuri": "code.zip", + "contenturi": "layer1.zip", + "definitionuri": "swagger1.json", + "templateurl": "template.yaml", +} diff --git a/integration/helpers/resource.py b/integration/helpers/resource.py new file mode 100644 index 0000000000..0f4632303f --- /dev/null +++ b/integration/helpers/resource.py @@ -0,0 +1,153 @@ +import json +import re +import random +import string # pylint: disable=deprecated-module + +from integration.helpers.yaml_utils import load_yaml + +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + +import boto3 +from botocore.exceptions import ClientError, NoRegionError + +from samtranslator.translator.logical_id_generator import LogicalIdGenerator + +# Length of the random suffix added at the end of the resources we create +# to avoid collisions between tests +RANDOM_SUFFIX_LENGTH = 12 + + +def verify_stack_resources(expected_file_path, stack_resources): + """ + Verifies that the stack resources match the expected ones + + Parameters + ---------- + expected_file_path : Path + Path to the file containing the expected resources + stack_resources : List + Stack resources + + Returns + ------- + bool + True if the stack resources exactly match the expected ones, False otherwise + """ + with open(expected_file_path) as expected_data: + expected_resources = _sort_resources(json.load(expected_data)) + parsed_resources = _sort_resources(stack_resources["StackResourceSummaries"]) + + if len(expected_resources) != len(parsed_resources): + return "'{}' resources expected, '{}' found".format(len(expected_resources), len(parsed_resources)) + + for i in range(len(expected_resources)): + exp = expected_resources[i] + parsed = parsed_resources[i] + if not re.match( + "^" + exp["LogicalResourceId"] + "([0-9a-f]{" + str(LogicalIdGenerator.HASH_LENGTH) + "})?$", + parsed["LogicalResourceId"], + ): + parsed_trimed_down = { + "LogicalResourceId": parsed["LogicalResourceId"], + "ResourceType": parsed["ResourceType"], + } + + return "'{}' expected, '{}' found (Resources must appear in the same order, don't include the LogicalId random suffix)".format( + exp, parsed_trimed_down + ) + if exp["ResourceType"] != parsed["ResourceType"]: + return "'{}' expected, '{}' found".format(exp["ResourceType"], parsed["ResourceType"]) + return None + + +def generate_suffix(): + """ + Generates a basic random string of length RANDOM_SUFFIX_LENGTH + to append to objects names used in the tests to avoid collisions + between tests runs + + Returns + ------- + string + Random lowercase alphanumeric string of length RANDOM_SUFFIX_LENGTH + """ + return "".join(random.choice(string.ascii_lowercase) for i in range(RANDOM_SUFFIX_LENGTH)) + + +def _sort_resources(resources): + """ + Sorts a stack's resources by LogicalResourceId + + Parameters + ---------- + resources : list + Resources to sort + + Returns + ------- + list + List of resources, sorted + """ + if resources is None: + return [] + return sorted(resources, key=lambda d: d["LogicalResourceId"]) + + +def create_bucket(bucket_name, region): + """ + Creates a S3 bucket in a specific region + + Parameters + ---------- + bucket_name : string + Bucket name + region : string + Region name + + Raises + ------ + NoRegionError + If region is not specified + """ + if region is None: + raise NoRegionError() + if region == "us-east-1": + s3_client = boto3.client("s3") + s3_client.create_bucket(Bucket=bucket_name) + else: + s3_client = boto3.client("s3", region_name=region) + location = {"LocationConstraint": region} + s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) + + +def current_region_does_not_support(services): + """ + Decide if a test should be skipped in the current testing region with the specific resources + + Parameters + ---------- + services : List + the services to be tested in the current testing region + + Returns + ------- + Boolean + If skip return true otherwise false + """ + + session = boto3.session.Session() + region = session.region_name + + tests_integ_dir = Path(__file__).resolve().parents[1] + config_dir = Path(tests_integ_dir, "config") + region_exclude_services_file = str(Path(config_dir, "region_service_exclusion.yaml")) + region_exclude_services = load_yaml(region_exclude_services_file) + + if region not in region_exclude_services["regions"]: + return False + + # check if any one of the services is in the excluded services for current testing region + return bool(set(services).intersection(set(region_exclude_services["regions"][region]))) diff --git a/integration/helpers/template.py b/integration/helpers/template.py new file mode 100644 index 0000000000..f75bc4c56e --- /dev/null +++ b/integration/helpers/template.py @@ -0,0 +1,42 @@ +import json +import logging +from functools import reduce + +import boto3 + +from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.transform import transform +from samtranslator.yaml_helper import yaml_parse + + +def transform_template(sam_template_path, cfn_output_path): + """ + Locally transforms a SAM template to a Cloud Formation template + + Parameters + ---------- + sam_template_path : Path + SAM template input path + cfn_output_path : Path + Cloud formation template output path + """ + LOG = logging.getLogger(__name__) + iam_client = boto3.client("iam") + + with open(sam_template_path) as f: + sam_template = yaml_parse(f) + + try: + cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2) + + with open(cfn_output_path, "w") as f: + f.write(cloud_formation_template_prettified) + + print("Wrote transformed CloudFormation template to: " + cfn_output_path) + except InvalidDocumentException as e: + error_message = reduce(lambda message, error: message + " " + error.message, e.causes, e.message) + LOG.error(error_message) + errors = map(lambda cause: cause.message, e.causes) + LOG.error(errors) diff --git a/integration/helpers/yaml_utils.py b/integration/helpers/yaml_utils.py new file mode 100644 index 0000000000..09365d6eb5 --- /dev/null +++ b/integration/helpers/yaml_utils.py @@ -0,0 +1,35 @@ +import yaml + + +def load_yaml(file_path): + """ + Loads a yaml file + + Parameters + ---------- + file_path : Path + File path + + Returns + ------- + Object + Yaml object + """ + with open(file_path) as f: + data = f.read() + return yaml.load(data, Loader=yaml.FullLoader) + + +def dump_yaml(file_path, yaml_doc): + """ + Writes a yaml object to a file + + Parameters + ---------- + file_path : Path + File path + yaml_doc : Object + Yaml object + """ + with open(file_path, "w") as f: + yaml.dump(yaml_doc, f) diff --git a/integration/resources/code/code.zip b/integration/resources/code/code.zip new file mode 100644 index 0000000000..cf321b5961 Binary files /dev/null and b/integration/resources/code/code.zip differ diff --git a/integration/resources/code/layer1.zip b/integration/resources/code/layer1.zip new file mode 100644 index 0000000000..c628f71d5b Binary files /dev/null and b/integration/resources/code/layer1.zip differ diff --git a/integration/resources/code/swagger1.json b/integration/resources/code/swagger1.json new file mode 100644 index 0000000000..81d235a108 --- /dev/null +++ b/integration/resources/code/swagger1.json @@ -0,0 +1,340 @@ +{ + "swagger": "2.0", + "info": { + "title": "PetStore", + "description": "Your first API with Amazon API Gateway. This is a sample API that integrates via HTTP with our demo Pet Store endpoints" + }, + "schemes": [ + "https" + ], + "paths": { + "/": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "text/html" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Content-Type": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Content-Type": "'text/html'" + }, + "responseTemplates": { + "text/html": "\n \n \n \n \n

Welcome to your Pet Store API

\n

\n You have succesfully deployed your first API. You are seeing this HTML page because the GET method to the root resource of your API returns this content as a Mock integration.\n

\n

\n The Pet Store API contains the /pets and /pets/{petId} resources. By making a GET request to /pets you can retrieve a list of Pets in your API. If you are looking for a specific pet, for example the pet with ID 1, you can make a GET request to /pets/1.\n

\n

\n You can use a REST client such as Postman to test the POST methods in your API to create a new pet. Use the sample body below to send the POST request:\n

\n
\n{\n    \"type\" : \"cat\",\n    \"price\" : 123.11\n}\n        
\n \n" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + }, + "post": { + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "POST", + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + }, + "/pets": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "page", + "in": "query", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "GET", + "requestParameters": { + "integration.request.querystring.page": "method.request.querystring.page", + "integration.request.querystring.type": "method.request.querystring.type" + }, + "type": "http" + } + }, + "post": { + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "POST", + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'POST,GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + }, + "/pets/{petId}": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets/{petId}", + "httpMethod": "GET", + "requestParameters": { + "integration.request.path.petId": "method.request.path.petId" + }, + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + } + }, + "definitions": { + "Empty": { + "type": "object" + } + } +} diff --git a/integration/resources/code/swagger2.json b/integration/resources/code/swagger2.json new file mode 100644 index 0000000000..7db53a271b --- /dev/null +++ b/integration/resources/code/swagger2.json @@ -0,0 +1,215 @@ +{ + "swagger": "2.0", + "info": { + "title": "PetStore", + "description": "Your first API with Amazon API Gateway. This is a sample API that integrates via HTTP with our demo Pet Store endpoints" + }, + "schemes": [ + "https" + ], + "paths": { + "/": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "text/html" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Content-Type": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Content-Type": "'text/html'" + }, + "responseTemplates": { + "text/html": "\n \n \n \n \n

Welcome to your Pet Store API

\n

\n You have succesfully deployed your first API. You are seeing this HTML page because the GET method to the root resource of your API returns this content as a Mock integration.\n

\n

\n The Pet Store API contains the /pets and /pets/{petId} resources. By making a GET request to /pets you can retrieve a list of Pets in your API. If you are looking for a specific pet, for example the pet with ID 1, you can make a GET request to /pets/1.\n

\n

\n You can use a REST client such as Postman to test the POST methods in your API to create a new pet. Use the sample body below to send the POST request:\n

\n
\n{\n    \"type\" : \"cat\",\n    \"price\" : 123.11\n}\n        
\n \n" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + }, + "post": { + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets", + "httpMethod": "POST", + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + }, + "/pets/{petId}": { + "get": { + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "uri": "http://petstore-demo-endpoint.execute-api.com/petstore/pets/{petId}", + "httpMethod": "GET", + "requestParameters": { + "integration.request.path.petId": "method.request.path.petId" + }, + "type": "http" + } + }, + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/Empty" + }, + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "type": "mock" + } + } + } + }, + "definitions": { + "Empty": { + "type": "object" + } + } +} diff --git a/integration/resources/code/template.yaml b/integration/resources/code/template.yaml new file mode 100644 index 0000000000..1949e8ba61 --- /dev/null +++ b/integration/resources/code/template.yaml @@ -0,0 +1,8 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Resources: + MyTable: + Type: 'AWS::Serverless::SimpleTable' +Outputs: + TableName: + Value: !Ref MyTable \ No newline at end of file diff --git a/integration/resources/expected/single/basic_api.json b/integration/resources/expected/single/basic_api.json new file mode 100644 index 0000000000..d45cb8ce34 --- /dev/null +++ b/integration/resources/expected/single/basic_api.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/integration/resources/expected/single/basic_api_inline_openapi.json b/integration/resources/expected/single/basic_api_inline_openapi.json new file mode 100644 index 0000000000..d45cb8ce34 --- /dev/null +++ b/integration/resources/expected/single/basic_api_inline_openapi.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/integration/resources/expected/single/basic_api_inline_swagger.json b/integration/resources/expected/single/basic_api_inline_swagger.json new file mode 100644 index 0000000000..d45cb8ce34 --- /dev/null +++ b/integration/resources/expected/single/basic_api_inline_swagger.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/integration/resources/expected/single/basic_api_inline_with_cache.json b/integration/resources/expected/single/basic_api_inline_with_cache.json new file mode 100644 index 0000000000..d45cb8ce34 --- /dev/null +++ b/integration/resources/expected/single/basic_api_inline_with_cache.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiMyNewStageNameStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/integration/resources/expected/single/basic_api_inline_with_tags.json b/integration/resources/expected/single/basic_api_inline_with_tags.json new file mode 100644 index 0000000000..84d8e643ff --- /dev/null +++ b/integration/resources/expected/single/basic_api_inline_with_tags.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiStage", "ResourceType":"AWS::ApiGateway::Stage" } +] diff --git a/integration/resources/expected/single/basic_api_with_tags.json b/integration/resources/expected/single/basic_api_with_tags.json new file mode 100644 index 0000000000..d636093ca0 --- /dev/null +++ b/integration/resources/expected/single/basic_api_with_tags.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGateway::RestApi" }, + { "LogicalResourceId":"MyApiDeployment", "ResourceType":"AWS::ApiGateway::Deployment" }, + { "LogicalResourceId":"MyApiStage", "ResourceType":"AWS::ApiGateway::Stage" } +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_application_s3_location.json b/integration/resources/expected/single/basic_application_s3_location.json new file mode 100644 index 0000000000..a033c90b76 --- /dev/null +++ b/integration/resources/expected/single/basic_application_s3_location.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyNestedApp", "ResourceType":"AWS::CloudFormation::Stack" } +] diff --git a/integration/resources/expected/single/basic_application_sar_location.json b/integration/resources/expected/single/basic_application_sar_location.json new file mode 100644 index 0000000000..a033c90b76 --- /dev/null +++ b/integration/resources/expected/single/basic_application_sar_location.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyNestedApp", "ResourceType":"AWS::CloudFormation::Stack" } +] diff --git a/integration/resources/expected/single/basic_application_sar_location_with_intrinsics.json b/integration/resources/expected/single/basic_application_sar_location_with_intrinsics.json new file mode 100644 index 0000000000..884f93d12e --- /dev/null +++ b/integration/resources/expected/single/basic_application_sar_location_with_intrinsics.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyNestedApp", "ResourceType":"AWS::CloudFormation::Stack" }, + { "LogicalResourceId":"MySns", "ResourceType":"AWS::SNS::Topic" } +] diff --git a/integration/resources/expected/single/basic_function.json b/integration/resources/expected/single/basic_function.json new file mode 100644 index 0000000000..2cb129f54d --- /dev/null +++ b/integration/resources/expected/single/basic_function.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" }, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] diff --git a/integration/resources/expected/single/basic_function_event_destinations.json b/integration/resources/expected/single/basic_function_event_destinations.json new file mode 100644 index 0000000000..3588c9d130 --- /dev/null +++ b/integration/resources/expected/single/basic_function_event_destinations.json @@ -0,0 +1,14 @@ +[ + { "LogicalResourceId":"MyTestFunction2", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2EventInvokeConfigOnSuccessTopic", "ResourceType":"AWS::SNS::Topic" }, + { "LogicalResourceId":"MyTestFunctionRole", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"MyTestFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2Aliaslive", "ResourceType":"AWS::Lambda::Alias"}, + { "LogicalResourceId":"MyTestFunctionEventInvokeConfigOnSuccessQueue", "ResourceType":"AWS::SQS::Queue"}, + { "LogicalResourceId":"MyTestFunction2Role", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"DestinationLambda", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyTestFunction2EventInvokeConfig", "ResourceType":"AWS::Lambda::EventInvokeConfig"}, + { "LogicalResourceId":"MyTestFunction2Version", "ResourceType":"AWS::Lambda::Version"}, + { "LogicalResourceId":"MyTestFunctionEventInvokeConfig", "ResourceType":"AWS::Lambda::EventInvokeConfig"}, + { "LogicalResourceId":"DestinationLambdaRole", "ResourceType":"AWS::IAM::Role"} +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_function_no_envvar.json b/integration/resources/expected/single/basic_function_no_envvar.json new file mode 100644 index 0000000000..98d62e4bbd --- /dev/null +++ b/integration/resources/expected/single/basic_function_no_envvar.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_function_openapi.json b/integration/resources/expected/single/basic_function_openapi.json new file mode 100644 index 0000000000..98d62e4bbd --- /dev/null +++ b/integration/resources/expected/single/basic_function_openapi.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_function_with_kmskeyarn.json b/integration/resources/expected/single/basic_function_with_kmskeyarn.json new file mode 100644 index 0000000000..5cba0276b4 --- /dev/null +++ b/integration/resources/expected/single/basic_function_with_kmskeyarn.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"BasicFunctionWithKmsKeyArn", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"BasicFunctionWithKmsKeyArnRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyKey", "ResourceType":"AWS::KMS::Key" } +] diff --git a/integration/resources/expected/single/basic_function_with_sns_dlq.json b/integration/resources/expected/single/basic_function_with_sns_dlq.json new file mode 100644 index 0000000000..3ded5dd12c --- /dev/null +++ b/integration/resources/expected/single/basic_function_with_sns_dlq.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyTopic", "ResourceType":"AWS::SNS::Topic" } +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_function_with_sqs_dlq.json b/integration/resources/expected/single/basic_function_with_sqs_dlq.json new file mode 100644 index 0000000000..a29c8734bc --- /dev/null +++ b/integration/resources/expected/single/basic_function_with_sqs_dlq.json @@ -0,0 +1,5 @@ +[ + { "LogicalResourceId":"MyFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyQueue", "ResourceType":"AWS::SQS::Queue" } +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_function_with_tags.json b/integration/resources/expected/single/basic_function_with_tags.json new file mode 100644 index 0000000000..4e83eaf6b0 --- /dev/null +++ b/integration/resources/expected/single/basic_function_with_tags.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" } +] diff --git a/integration/resources/expected/single/basic_function_with_tracing.json b/integration/resources/expected/single/basic_function_with_tracing.json new file mode 100644 index 0000000000..e47b43ddd8 --- /dev/null +++ b/integration/resources/expected/single/basic_function_with_tracing.json @@ -0,0 +1,6 @@ +[ + { "LogicalResourceId":"ActiveTracingFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"ActiveTracingFunctionRole", "ResourceType":"AWS::IAM::Role"}, + { "LogicalResourceId":"PassThroughTracingFunction", "ResourceType":"AWS::Lambda::Function"}, + { "LogicalResourceId":"PassThroughTracingFunctionRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_http_api.json b/integration/resources/expected/single/basic_http_api.json new file mode 100644 index 0000000000..7fac895cbd --- /dev/null +++ b/integration/resources/expected/single/basic_http_api.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGatewayV2::Api"}, + { "LogicalResourceId":"MyApiApiGatewayDefaultStage", "ResourceType":"AWS::ApiGatewayV2::Stage" } +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_layer.json b/integration/resources/expected/single/basic_layer.json new file mode 100644 index 0000000000..beba3153c7 --- /dev/null +++ b/integration/resources/expected/single/basic_layer.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"} +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_layer_with_parameters.json b/integration/resources/expected/single/basic_layer_with_parameters.json new file mode 100644 index 0000000000..beba3153c7 --- /dev/null +++ b/integration/resources/expected/single/basic_layer_with_parameters.json @@ -0,0 +1,3 @@ +[ + { "LogicalResourceId":"MyLayerVersion", "ResourceType":"AWS::Lambda::LayerVersion"} +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_state_machine_inline_definition.json b/integration/resources/expected/single/basic_state_machine_inline_definition.json new file mode 100644 index 0000000000..4d778a7e46 --- /dev/null +++ b/integration/resources/expected/single/basic_state_machine_inline_definition.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyBasicStateMachine", "ResourceType":"AWS::StepFunctions::StateMachine"}, + { "LogicalResourceId":"MyBasicStateMachineRole", "ResourceType":"AWS::IAM::Role" } +] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_state_machine_with_tags.json b/integration/resources/expected/single/basic_state_machine_with_tags.json new file mode 100644 index 0000000000..976394f6bf --- /dev/null +++ b/integration/resources/expected/single/basic_state_machine_with_tags.json @@ -0,0 +1,4 @@ +[ + { "LogicalResourceId":"MyStateMachine", "ResourceType":"AWS::StepFunctions::StateMachine"}, + { "LogicalResourceId":"MyStateMachineRole", "ResourceType":"AWS::IAM::Role" } +] diff --git a/integration/resources/expected/single/function_alias_with_http_api_events.json b/integration/resources/expected/single/function_alias_with_http_api_events.json new file mode 100644 index 0000000000..8001cf476b --- /dev/null +++ b/integration/resources/expected/single/function_alias_with_http_api_events.json @@ -0,0 +1,10 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" }, + { "LogicalResourceId":"MyLambdaFunctionAliaslive", "ResourceType":"AWS::Lambda::Alias" }, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyLambdaFunctionVersion", "ResourceType":"AWS::Lambda::Version" }, + { "LogicalResourceId":"MyLambdaFunctionFooEventPermission", "ResourceType":"AWS::Lambda::Permission" }, + { "LogicalResourceId":"MyLambdaFunctionBarEventPermission", "ResourceType":"AWS::Lambda::Permission" }, + { "LogicalResourceId":"MyHttpApi", "ResourceType":"AWS::ApiGatewayV2::Api" }, + { "LogicalResourceId":"MyHttpApiApiGatewayDefaultStage", "ResourceType":"AWS::ApiGatewayV2::Stage" } +] diff --git a/integration/resources/expected/single/function_with_deployment_preference_alarms_intrinsic_if.json b/integration/resources/expected/single/function_with_deployment_preference_alarms_intrinsic_if.json new file mode 100644 index 0000000000..c71bcec273 --- /dev/null +++ b/integration/resources/expected/single/function_with_deployment_preference_alarms_intrinsic_if.json @@ -0,0 +1,30 @@ +[ + { + "LogicalResourceId": "ServerlessDeploymentApplication", + "ResourceType": "AWS::CodeDeploy::Application" + }, + { + "LogicalResourceId": "MyLambdaFunctionRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "CodeDeployServiceRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "MyLambdaFunction", + "ResourceType": "AWS::Lambda::Function" + }, + { + "LogicalResourceId": "MyLambdaFunctionDeploymentGroup", + "ResourceType": "AWS::CodeDeploy::DeploymentGroup" + }, + { + "LogicalResourceId": "MyLambdaFunctionVersion", + "ResourceType": "AWS::Lambda::Version" + }, + { + "LogicalResourceId": "MyLambdaFunctionAliaslive", + "ResourceType": "AWS::Lambda::Alias" + } +] \ No newline at end of file diff --git a/integration/resources/expected/single/function_with_http_api_events.json b/integration/resources/expected/single/function_with_http_api_events.json new file mode 100644 index 0000000000..14e65d3101 --- /dev/null +++ b/integration/resources/expected/single/function_with_http_api_events.json @@ -0,0 +1,7 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" }, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyLambdaFunctionFooEventPermission", "ResourceType":"AWS::Lambda::Permission" }, + { "LogicalResourceId":"MyHttpApi", "ResourceType":"AWS::ApiGatewayV2::Api" }, + { "LogicalResourceId":"MyHttpApiApiGatewayDefaultStage", "ResourceType":"AWS::ApiGatewayV2::Stage" } +] diff --git a/integration/resources/templates/single/basic_api.yaml b/integration/resources/templates/single/basic_api.yaml new file mode 100644 index 0000000000..0b6322cec0 --- /dev/null +++ b/integration/resources/templates/single/basic_api.yaml @@ -0,0 +1,6 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionUri: ${definitionuri} diff --git a/integration/resources/templates/single/basic_api_inline_openapi.yaml b/integration/resources/templates/single/basic_api_inline_openapi.yaml new file mode 100644 index 0000000000..7f14ad5dc3 --- /dev/null +++ b/integration/resources/templates/single/basic_api_inline_openapi.yaml @@ -0,0 +1,27 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionBody: + # Simple HTTP Proxy API + openapi: "3.0" + info: + version: "2016-09-23T22:23:23Z" + title: "Simple Api" + basePath: "/demo" + schemes: + - "https" + paths: + /http/{proxy+}: + x-amazon-apigateway-any-method: + parameters: + - name: "proxy" + in: "path" + x-amazon-apigateway-integration: + type: "http_proxy" + uri: "http://httpbin.org/{proxy}" + httpMethod: "ANY" + passthroughBehavior: "when_no_match" + requestParameters: + integration.request.path.proxy: "method.request.path.proxy" diff --git a/integration/resources/templates/single/basic_api_inline_swagger.yaml b/integration/resources/templates/single/basic_api_inline_swagger.yaml new file mode 100644 index 0000000000..ce9ff24298 --- /dev/null +++ b/integration/resources/templates/single/basic_api_inline_swagger.yaml @@ -0,0 +1,27 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: MyNewStageName + DefinitionBody: + # Simple HTTP Proxy API + swagger: "2.0" + info: + version: "2016-09-23T22:23:23Z" + title: "Simple Api" + basePath: "/demo" + schemes: + - "https" + paths: + /http/{proxy+}: + x-amazon-apigateway-any-method: + parameters: + - name: "proxy" + in: "path" + x-amazon-apigateway-integration: + type: "http_proxy" + uri: "http://httpbin.org/{proxy}" + httpMethod: "ANY" + passthroughBehavior: "when_no_match" + requestParameters: + integration.request.path.proxy: "method.request.path.proxy" diff --git a/integration/resources/templates/single/basic_api_with_tags.yaml b/integration/resources/templates/single/basic_api_with_tags.yaml new file mode 100644 index 0000000000..c3bce8d8d4 --- /dev/null +++ b/integration/resources/templates/single/basic_api_with_tags.yaml @@ -0,0 +1,9 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: my-new-stage-name + DefinitionUri: ${definitionuri} + Tags: + TagKey1: TagValue1 + TagKey2: "" diff --git a/integration/resources/templates/single/basic_application_s3_location.yaml b/integration/resources/templates/single/basic_application_s3_location.yaml new file mode 100644 index 0000000000..5a60446747 --- /dev/null +++ b/integration/resources/templates/single/basic_application_s3_location.yaml @@ -0,0 +1,5 @@ +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: ${templateurl} diff --git a/integration/resources/templates/single/basic_application_sar_location.yaml b/integration/resources/templates/single/basic_application_sar_location.yaml new file mode 100644 index 0000000000..5bd59c2e49 --- /dev/null +++ b/integration/resources/templates/single/basic_application_sar_location.yaml @@ -0,0 +1,9 @@ +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python + SemanticVersion: 1.0.2 + Parameters: + IdentityNameParameter: test diff --git a/integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml b/integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml new file mode 100644 index 0000000000..471af7ae00 --- /dev/null +++ b/integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml @@ -0,0 +1,57 @@ +Parameters: + SemanticVersion: + Type: String + Default: 1.0.2 + +Mappings: + SARApplication: + us-east-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python + us-east-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + us-west-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + us-west-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-central-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + eu-west-3: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-south-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-northeast-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-northeast-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-southeast-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ap-southeast-2: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + ca-central-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + sa-east-1: + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 + +Resources: + MyNestedApp: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: + Fn::FindInMap: + - SARApplication + - {Ref: 'AWS::Region'} + - ApplicationId + SemanticVersion: + Ref: SemanticVersion + Parameters: + IdentityNameParameter: test + NotificationARNs: + - Ref: MySns + + MySns: + Type: AWS::SNS::Topic \ No newline at end of file diff --git a/integration/resources/templates/single/basic_function.yaml b/integration/resources/templates/single/basic_function.yaml new file mode 100644 index 0000000000..d3fdf30784 --- /dev/null +++ b/integration/resources/templates/single/basic_function.yaml @@ -0,0 +1,15 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Environment: + Variables: + Name: Value + Name2: Value2 diff --git a/integration/resources/templates/single/basic_function_event_destinations.yaml b/integration/resources/templates/single/basic_function_event_destinations.yaml new file mode 100644 index 0000000000..2129313b1b --- /dev/null +++ b/integration/resources/templates/single/basic_function_event_destinations.yaml @@ -0,0 +1,95 @@ +Conditions: + QueueCreationDisabled: + Fn::Equals: + - false + - true +Resources: + MyTestFunction: + Type: AWS::Serverless::Function + Properties: + EventInvokeConfig: + MaximumEventAgeInSeconds: 70 + MaximumRetryAttempts: 1 + DestinationConfig: + OnSuccess: + Type: SQS + Destination: + Fn::If: + - QueueCreationDisabled + - Fn::GetAtt: + - DestinationSQS + - Arn + - Ref: 'AWS::NoValue' + OnFailure: + Type: Lambda + Destination: + Fn::GetAtt: + - DestinationLambda + - Arn + InlineCode: | + exports.handler = function(event, context, callback) { + var event_received_at = new Date().toISOString(); + console.log('Event received at: ' + event_received_at); + console.log('Received event:', JSON.stringify(event, null, 2)); + if (event.Success) { + console.log("Success"); + context.callbackWaitsForEmptyEventLoop = false; + callback(null); + } else { + console.log("Failure"); + context.callbackWaitsForEmptyEventLoop = false; + callback(new Error("Failure from event, Success = false, I am failing!"), 'Destination Function Error Thrown'); + } + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + MyTestFunction2: + Type: AWS::Serverless::Function + Properties: + AutoPublishAlias: live + EventInvokeConfig: + MaximumEventAgeInSeconds: 80 + MaximumRetryAttempts: 2 + DestinationConfig: + OnSuccess: + Type: SNS + OnFailure: + Type: EventBridge + Destination: + Fn::Sub: arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default + InlineCode: | + exports.handler = function(event, context, callback) { + var event_received_at = new Date().toISOString(); + console.log('Event received at: ' + event_received_at); + console.log('Received event:', JSON.stringify(event, null, 2)); + if (event.Success) { + console.log("Success"); + context.callbackWaitsForEmptyEventLoop = false; + callback(null); + } else { + console.log("Failure"); + context.callbackWaitsForEmptyEventLoop = false; + callback(new Error("Failure from event, Success = false, I am failing!"), 'Destination Function Error Thrown'); + } + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + DestinationLambda: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (event) => { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + return response; + }; + Handler: index.handler + Runtime: nodejs10.x + MemorySize: 1024 + DestinationSQS: + Condition: QueueCreationDisabled + Type: AWS::SQS::Queue diff --git a/integration/resources/templates/single/basic_function_no_envvar.yaml b/integration/resources/templates/single/basic_function_no_envvar.yaml new file mode 100644 index 0000000000..6ecedfd923 --- /dev/null +++ b/integration/resources/templates/single/basic_function_no_envvar.yaml @@ -0,0 +1,11 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess diff --git a/integration/resources/templates/single/basic_function_openapi.yaml b/integration/resources/templates/single/basic_function_openapi.yaml new file mode 100644 index 0000000000..359f7d58bc --- /dev/null +++ b/integration/resources/templates/single/basic_function_openapi.yaml @@ -0,0 +1,18 @@ +Globals: + Api: + OpenApiVersion: 3.0.1 +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Environment: + Variables: + Name: Value + Name2: Value2 diff --git a/integration/resources/templates/single/basic_function_with_kmskeyarn.yaml b/integration/resources/templates/single/basic_function_with_kmskeyarn.yaml new file mode 100644 index 0000000000..91a9fc05ff --- /dev/null +++ b/integration/resources/templates/single/basic_function_with_kmskeyarn.yaml @@ -0,0 +1,32 @@ +Resources: + BasicFunctionWithKmsKeyArn: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Environment: + Variables: + Key: Value + KmsKeyArn: + Fn::GetAtt: [MyKey, Arn] + + + 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: + Fn::Sub: "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" + Action: + - "kms:*" + Resource: "*" \ No newline at end of file diff --git a/integration/resources/templates/single/basic_function_with_sns_dlq.yaml b/integration/resources/templates/single/basic_function_with_sns_dlq.yaml new file mode 100644 index 0000000000..0d4435fef0 --- /dev/null +++ b/integration/resources/templates/single/basic_function_with_sns_dlq.yaml @@ -0,0 +1,14 @@ +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + DeadLetterQueue: + Type: SNS + TargetArn: + Ref: "MyTopic" + + MyTopic: + Type: AWS::SNS::Topic diff --git a/integration/resources/templates/single/basic_function_with_sqs_dlq.yaml b/integration/resources/templates/single/basic_function_with_sqs_dlq.yaml new file mode 100644 index 0000000000..26aa16b01a --- /dev/null +++ b/integration/resources/templates/single/basic_function_with_sqs_dlq.yaml @@ -0,0 +1,15 @@ + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + DeadLetterQueue: + Type: SQS + TargetArn: + Fn::GetAtt: ["MyQueue", "Arn"] + + MyQueue: + Type: AWS::SQS::Queue diff --git a/integration/resources/templates/single/basic_function_with_tags.yaml b/integration/resources/templates/single/basic_function_with_tags.yaml new file mode 100644 index 0000000000..cc815a52e8 --- /dev/null +++ b/integration/resources/templates/single/basic_function_with_tags.yaml @@ -0,0 +1,14 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tags: + TagKey1: TagValue1 + TagKey2: "" diff --git a/integration/resources/templates/single/basic_function_with_tracing.yaml b/integration/resources/templates/single/basic_function_with_tracing.yaml new file mode 100644 index 0000000000..f9fa0809cd --- /dev/null +++ b/integration/resources/templates/single/basic_function_with_tracing.yaml @@ -0,0 +1,36 @@ +Parameters: + Bucket: + Type: String + CodeKey: + Type: String + SwaggerKey: + Type: String + TracingParamPassThrough: + Type: String + Default: PassThrough + +Resources: + ActiveTracingFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tracing: Active + + PassThroughTracingFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + MemorySize: 128 + Policies: + - AWSLambdaRole + - AmazonS3ReadOnlyAccess + Tracing: + Ref: TracingParamPassThrough diff --git a/integration/resources/templates/single/basic_http_api.yaml b/integration/resources/templates/single/basic_http_api.yaml new file mode 100644 index 0000000000..27e3c340d2 --- /dev/null +++ b/integration/resources/templates/single/basic_http_api.yaml @@ -0,0 +1,11 @@ +Resources: + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + DefinitionBody: + info: + version: '1.0' + title: + Ref: AWS::StackName + openapi: 3.0.1 + paths: {} \ No newline at end of file diff --git a/integration/resources/templates/single/basic_layer.yaml b/integration/resources/templates/single/basic_layer.yaml new file mode 100644 index 0000000000..6b308fd91f --- /dev/null +++ b/integration/resources/templates/single/basic_layer.yaml @@ -0,0 +1,6 @@ +Resources: + MyLayerVersion: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ${contenturi} + RetentionPolicy: Delete diff --git a/integration/resources/templates/single/basic_layer_with_parameters.yaml b/integration/resources/templates/single/basic_layer_with_parameters.yaml new file mode 100644 index 0000000000..f5eb8fdb84 --- /dev/null +++ b/integration/resources/templates/single/basic_layer_with_parameters.yaml @@ -0,0 +1,46 @@ +Parameters: + Retention: + Type: String + Default: Retain + License: + Type: String + Default: MIT-0 + Runtimes: + Type: CommaDelimitedList + Default: nodejs12.x,nodejs10.x + LayerName: + Type: String + Default: MyNamedLayerVersion + Description: + Type: String + Default: Some description about this layer goes here + +Resources: + MyLayerVersion: + Type: AWS::Serverless::LayerVersion + Properties: + ContentUri: ${contenturi} + LayerName: + Ref: LayerName + RetentionPolicy: + Ref: Retention + CompatibleRuntimes: + Ref: Runtimes + LicenseInfo: + Ref: License + Description: + Ref: Description + +Outputs: + MyLayerArn: + Value: + Ref: MyLayerVersion + License: + Value: + Ref: License + Description: + Value: + Ref: Description + LayerName: + Value: + Ref: LayerName \ No newline at end of file diff --git a/integration/resources/templates/single/basic_state_machine_inline_definition.yaml b/integration/resources/templates/single/basic_state_machine_inline_definition.yaml new file mode 100644 index 0000000000..8fb8a16cde --- /dev/null +++ b/integration/resources/templates/single/basic_state_machine_inline_definition.yaml @@ -0,0 +1,23 @@ +Resources: + MyBasicStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Type: STANDARD + Definition: + Comment: A Hello World example of the Amazon States Language using Pass states + StartAt: Hello + States: + Hello: + Type: Pass + Result: Hello + Next: World + World: + Type: Pass + Result: World + End: true + Policies: + - Version: '2012-10-17' + Statement: + - Effect: Deny + Action: "*" + Resource: "*" diff --git a/integration/resources/templates/single/basic_state_machine_with_tags.yaml b/integration/resources/templates/single/basic_state_machine_with_tags.yaml new file mode 100644 index 0000000000..8156526419 --- /dev/null +++ b/integration/resources/templates/single/basic_state_machine_with_tags.yaml @@ -0,0 +1,33 @@ +Resources: + MyStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Definition: + Comment: A Hello World example of the Amazon States Language using Pass states + StartAt: Hello + States: + Hello: + Type: Pass + Result: Hello + Next: World + World: + Type: Pass + Result: World + End: true + Policies: + - Version: "2012-10-17" + Statement: + - Effect: Deny + Action: "*" + Resource: "*" + Tags: + TagOne: ValueOne + TagTwo: ValueTwo + Tracing: + Enabled: true + +Outputs: + MyStateMachineArn: + Description: ARN of the state machine + Value: + Ref: MyStateMachine diff --git a/integration/resources/templates/single/basic_table_no_param.yaml b/integration/resources/templates/single/basic_table_no_param.yaml new file mode 100644 index 0000000000..ae4fe8164d --- /dev/null +++ b/integration/resources/templates/single/basic_table_no_param.yaml @@ -0,0 +1,4 @@ +Resources: + MyApi: + Type: AWS::Serverless::SimpleTable + # SimpleTable does NOT require any parameters diff --git a/integration/resources/templates/single/basic_table_with_param.yaml b/integration/resources/templates/single/basic_table_with_param.yaml new file mode 100644 index 0000000000..c0a7039c3b --- /dev/null +++ b/integration/resources/templates/single/basic_table_with_param.yaml @@ -0,0 +1,12 @@ +Resources: + MyApi: + Type: AWS::Serverless::SimpleTable + Properties: + + PrimaryKey: + Name: mynewid + Type: Number + + ProvisionedThroughput: + ReadCapacityUnits: 2 + WriteCapacityUnits: 2 diff --git a/integration/resources/templates/single/function_alias_with_http_api_events.yaml b/integration/resources/templates/single/function_alias_with_http_api_events.yaml new file mode 100644 index 0000000000..88176be07a --- /dev/null +++ b/integration/resources/templates/single/function_alias_with_http_api_events.yaml @@ -0,0 +1,23 @@ +Resources: + MyHttpApi: + Type: AWS::Serverless::HttpApi + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + AutoPublishAlias: live + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + Events: + FooEvent: + Type: HttpApi + Properties: + ApiId: + Ref: MyHttpApi + BarEvent: + Type: HttpApi + Properties: + ApiId: + Ref: MyHttpApi + Path: /bar + Method: POST diff --git a/integration/resources/templates/single/function_with_deployment_preference_alarms_intrinsic_if.yaml b/integration/resources/templates/single/function_with_deployment_preference_alarms_intrinsic_if.yaml new file mode 100644 index 0000000000..261571ff5e --- /dev/null +++ b/integration/resources/templates/single/function_with_deployment_preference_alarms_intrinsic_if.yaml @@ -0,0 +1,23 @@ +Conditions: + MyCondition: + Fn::Equals: + - true + - false +Resources: + MyLambdaFunction: + Type: "AWS::Serverless::Function" + Properties: + CodeUri: ${codeuri} + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + DeploymentPreference: + Type: Linear10PercentEvery3Minutes + Alarms: + Fn::If: + - MyCondition + - - Alarm1 + - Alarm2 + - Alarm3 + - - Alarm1 + - Alarm5 diff --git a/integration/resources/templates/single/function_with_http_api_events.yaml b/integration/resources/templates/single/function_with_http_api_events.yaml new file mode 100644 index 0000000000..1ffbe05f73 --- /dev/null +++ b/integration/resources/templates/single/function_with_http_api_events.yaml @@ -0,0 +1,22 @@ +Resources: + MyHttpApi: + Type: AWS::Serverless::HttpApi + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: nodejs12.x + CodeUri: ${codeuri} + Events: + FooEvent: + Type: HttpApi + Properties: + ApiId: + Ref: MyHttpApi + BarEvent: + Type: HttpApi + Properties: + ApiId: + Ref: MyHttpApi + Path: /bar + Method: POST diff --git a/integration/single/__init__.py b/integration/single/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/integration/single/__init__.py @@ -0,0 +1 @@ + diff --git a/integration/single/test_basic_api.py b/integration/single/test_basic_api.py new file mode 100644 index 0000000000..5e308d40da --- /dev/null +++ b/integration/single/test_basic_api.py @@ -0,0 +1,79 @@ +from integration.helpers.base_test import BaseTest + + +class TestBasicApi(BaseTest): + """ + Basic AWS::Serverless::Api tests + """ + + def test_basic_api(self): + """ + Creates an API and updates its DefinitionUri + """ + self.create_and_verify_stack("basic_api") + + first_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(first_dep_ids), 1) + + self.set_template_resource_property("MyApi", "DefinitionUri", self.get_s3_uri("swagger2.json")) + self.transform_template() + self.deploy_stack() + + second_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(second_dep_ids), 1) + + self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) + + def test_basic_api_inline_openapi(self): + """ + Creates an API with and inline OpenAPI and updates its DefinitionBody basePath + """ + self.create_and_verify_stack("basic_api_inline_openapi") + + first_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(first_dep_ids), 1) + + body = self.get_template_resource_property("MyApi", "DefinitionBody") + body["basePath"] = "/newDemo" + self.set_template_resource_property("MyApi", "DefinitionBody", body) + self.transform_template() + self.deploy_stack() + + second_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(second_dep_ids), 1) + + self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) + + def test_basic_api_inline_swagger(self): + """ + Creates an API with an inline Swagger and updates its DefinitionBody basePath + """ + self.create_and_verify_stack("basic_api_inline_swagger") + + first_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(first_dep_ids), 1) + + body = self.get_template_resource_property("MyApi", "DefinitionBody") + body["basePath"] = "/newDemo" + self.set_template_resource_property("MyApi", "DefinitionBody", body) + self.transform_template() + self.deploy_stack() + + second_dep_ids = self.get_stack_deployment_ids() + self.assertEqual(len(second_dep_ids), 1) + + self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) + + def test_basic_api_with_tags(self): + """ + Creates an API with tags + """ + self.create_and_verify_stack("basic_api_with_tags") + + stages = self.get_api_stack_stages() + self.assertEqual(len(stages), 2) + + stage = next((s for s in stages if s["stageName"] == "my-new-stage-name")) + self.assertIsNotNone(stage) + self.assertEqual(stage["tags"]["TagKey1"], "TagValue1") + self.assertEqual(stage["tags"]["TagKey2"], "") diff --git a/integration/single/test_basic_application.py b/integration/single/test_basic_application.py new file mode 100644 index 0000000000..43dc9fdfa7 --- /dev/null +++ b/integration/single/test_basic_application.py @@ -0,0 +1,57 @@ +from unittest.case import skipIf + +from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support + + +class TestBasicApplication(BaseTest): + """ + Basic AWS::Serverless::Application tests + """ + + @skipIf( + current_region_does_not_support(["ServerlessRepo"]), "ServerlessRepo is not supported in this testing region" + ) + def test_basic_application_s3_location(self): + """ + Creates an application with its properties defined as a template + file in a S3 bucket + """ + self.create_and_verify_stack("basic_application_s3_location") + + nested_stack_resource = self.get_stack_nested_stack_resources() + tables = self.get_stack_resources("AWS::DynamoDB::Table", nested_stack_resource) + + self.assertEqual(len(tables), 1) + self.assertEqual(tables[0]["LogicalResourceId"], "MyTable") + + @skipIf( + current_region_does_not_support(["ServerlessRepo"]), "ServerlessRepo is not supported in this testing region" + ) + def test_basic_application_sar_location(self): + """ + Creates an application with a lamda function + """ + self.create_and_verify_stack("basic_application_sar_location") + + nested_stack_resource = self.get_stack_nested_stack_resources() + functions = self.get_stack_resources("AWS::Lambda::Function", nested_stack_resource) + + self.assertEqual(len(functions), 1) + self.assertEqual(functions[0]["LogicalResourceId"], "helloworldpython") + + @skipIf( + current_region_does_not_support(["ServerlessRepo"]), "ServerlessRepo is not supported in this testing region" + ) + def test_basic_application_sar_location_with_intrinsics(self): + """ + Creates an application with a lambda function with intrinsics + """ + expected_function_name = "helloworldpython" if self.get_region() == "us-east-1" else "helloworldpython3" + self.create_and_verify_stack("basic_application_sar_location_with_intrinsics") + + nested_stack_resource = self.get_stack_nested_stack_resources() + functions = self.get_stack_resources("AWS::Lambda::Function", nested_stack_resource) + + self.assertEqual(len(functions), 1) + self.assertEqual(functions[0]["LogicalResourceId"], expected_function_name) diff --git a/integration/single/test_basic_function.py b/integration/single/test_basic_function.py new file mode 100644 index 0000000000..888ec66672 --- /dev/null +++ b/integration/single/test_basic_function.py @@ -0,0 +1,204 @@ +from unittest.case import skipIf + +import requests + +from integration.helpers.resource import current_region_does_not_support +from parameterized import parameterized +from integration.helpers.base_test import BaseTest + + +class TestBasicFunction(BaseTest): + """ + Basic AWS::Lambda::Function tests + """ + + @parameterized.expand( + [ + "basic_function", + "basic_function_no_envvar", + "basic_function_openapi", + ] + ) + def test_basic_function(self, file_name): + """ + Creates a basic lambda function + """ + self.create_and_verify_stack(file_name) + + self.set_template_resource_property("MyLambdaFunction", "Timeout", 10) + self.transform_template() + self.deploy_stack() + + self.assertEqual(self.get_resource_status_by_logical_id("MyLambdaFunction"), "UPDATE_COMPLETE") + + @parameterized.expand( + [ + "function_with_http_api_events", + "function_alias_with_http_api_events", + ] + ) + def test_function_with_http_api_events(self, file_name): + self.create_and_verify_stack(file_name) + + endpoint = self.get_api_v2_endpoint("MyHttpApi") + + self.assertEqual(requests.get(endpoint).text, self.FUNCTION_OUTPUT) + + def test_function_with_deployment_preference_alarms_intrinsic_if(self): + self.create_and_verify_stack("function_with_deployment_preference_alarms_intrinsic_if") + + @parameterized.expand( + [ + ("basic_function_with_sns_dlq", "sns:Publish"), + ("basic_function_with_sqs_dlq", "sqs:SendMessage"), + ] + ) + def test_basic_function_with_dlq(self, file_name, action): + """ + Creates a basic lambda function with dead letter queue policy + """ + dlq_policy_name = "DeadLetterQueuePolicy" + self.create_and_verify_stack(file_name) + + lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") + function_configuration = self.client_provider.lambda_client.get_function_configuration( + FunctionName=lambda_function_name + ) + dlq_arn = function_configuration["DeadLetterConfig"]["TargetArn"] + self.assertIsNotNone(dlq_arn, "DLQ Arn should be set") + + role_name = self.get_physical_id_by_type("AWS::IAM::Role") + role_policy_result = self.client_provider.iam_client.get_role_policy( + RoleName=role_name, PolicyName=dlq_policy_name + ) + statements = role_policy_result["PolicyDocument"]["Statement"] + + self.assertEqual(len(statements), 1, "Only one statement must be in policy") + self.assertEqual(statements[0]["Action"], action) + self.assertEqual(statements[0]["Resource"], dlq_arn) + self.assertEqual(statements[0]["Effect"], "Allow") + + @skipIf(current_region_does_not_support(["KMS"]), "KMS is not supported in this testing region") + def test_basic_function_with_kms_key_arn(self): + """ + Creates a basic lambda function with KMS key arn + """ + self.create_and_verify_stack("basic_function_with_kmskeyarn") + + lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") + function_configuration = self.client_provider.lambda_client.get_function_configuration( + FunctionName=lambda_function_name + ) + kms_key_arn = function_configuration["KMSKeyArn"] + + self.assertIsNotNone(kms_key_arn, "Expecting KmsKeyArn to be set.") + + def test_basic_function_with_tags(self): + """ + Creates a basic lambda function with tags + """ + self.create_and_verify_stack("basic_function_with_tags") + lambda_function_name = self.get_physical_id_by_type("AWS::Lambda::Function") + get_function_result = self.client_provider.lambda_client.get_function(FunctionName=lambda_function_name) + tags = get_function_result["Tags"] + + self.assertIsNotNone(tags, "Expecting tags on function.") + self.assertTrue("lambda:createdBy" in tags, "Expected 'lambda:CreatedBy' tag key, but not found.") + self.assertEqual("SAM", tags["lambda:createdBy"], "Expected 'SAM' tag value, but not found.") + self.assertTrue("TagKey1" in tags) + self.assertEqual(tags["TagKey1"], "TagValue1") + self.assertTrue("TagKey2" in tags) + self.assertEqual(tags["TagKey2"], "") + + def test_basic_function_event_destinations(self): + """ + Creates a basic lambda function with event destinations + """ + self.create_and_verify_stack("basic_function_event_destinations") + + test_function_1 = self.get_physical_id_by_logical_id("MyTestFunction") + test_function_2 = self.get_physical_id_by_logical_id("MyTestFunction2") + + function_invoke_config_result = self.client_provider.lambda_client.get_function_event_invoke_config( + FunctionName=test_function_1, Qualifier="$LATEST" + ) + self.assertIsNotNone( + function_invoke_config_result["DestinationConfig"], "Expecting destination config to be set." + ) + self.assertEqual( + int(function_invoke_config_result["MaximumEventAgeInSeconds"]), + 70, + "MaximumEventAgeInSeconds value is not set or incorrect.", + ) + self.assertEqual( + int(function_invoke_config_result["MaximumRetryAttempts"]), + 1, + "MaximumRetryAttempts value is not set or incorrect.", + ) + + function_invoke_config_result = self.client_provider.lambda_client.get_function_event_invoke_config( + FunctionName=test_function_2, Qualifier="live" + ) + self.assertIsNotNone( + function_invoke_config_result["DestinationConfig"], "Expecting destination config to be set." + ) + self.assertEqual( + int(function_invoke_config_result["MaximumEventAgeInSeconds"]), + 80, + "MaximumEventAgeInSeconds value is not set or incorrect.", + ) + self.assertEqual( + int(function_invoke_config_result["MaximumRetryAttempts"]), + 2, + "MaximumRetryAttempts value is not set or incorrect.", + ) + + @skipIf(current_region_does_not_support(["XRay"]), "XRay is not supported in this testing region") + def test_basic_function_with_tracing(self): + """ + Creates a basic lambda function with tracing + """ + parameters = [ + { + "ParameterKey": "Bucket", + "ParameterValue": self.s3_bucket_name, + "UsePreviousValue": False, + "ResolvedValue": "string", + }, + { + "ParameterKey": "CodeKey", + "ParameterValue": "code.zip", + "UsePreviousValue": False, + "ResolvedValue": "string", + }, + { + "ParameterKey": "SwaggerKey", + "ParameterValue": "swagger1.json", + "UsePreviousValue": False, + "ResolvedValue": "string", + }, + ] + self.create_and_verify_stack("basic_function_with_tracing", parameters) + + active_tracing_function_id = self.get_physical_id_by_logical_id("ActiveTracingFunction") + pass_through_tracing_function_id = self.get_physical_id_by_logical_id("PassThroughTracingFunction") + + function_configuration_result = self.client_provider.lambda_client.get_function_configuration( + FunctionName=active_tracing_function_id + ) + self.assertIsNotNone(function_configuration_result["TracingConfig"], "Expecting tracing config to be set.") + self.assertEqual( + function_configuration_result["TracingConfig"]["Mode"], + "Active", + "Expecting tracing config mode to be set to Active.", + ) + + function_configuration_result = self.client_provider.lambda_client.get_function_configuration( + FunctionName=pass_through_tracing_function_id + ) + self.assertIsNotNone(function_configuration_result["TracingConfig"], "Expecting tracing config to be set.") + self.assertEqual( + function_configuration_result["TracingConfig"]["Mode"], + "PassThrough", + "Expecting tracing config mode to be set to PassThrough.", + ) diff --git a/integration/single/test_basic_http_api.py b/integration/single/test_basic_http_api.py new file mode 100644 index 0000000000..e7f3c187d0 --- /dev/null +++ b/integration/single/test_basic_http_api.py @@ -0,0 +1,22 @@ +from unittest.case import skipIf + +from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support + + +class TestBasicHttpApi(BaseTest): + """ + Basic AWS::Serverless::HttpApi tests + """ + + @skipIf(current_region_does_not_support(["HttpApi"]), "HttpApi is not supported in this testing region") + def test_basic_http_api(self): + """ + Creates a HTTP API + """ + self.create_and_verify_stack("basic_http_api") + + stages = self.get_api_v2_stack_stages() + + self.assertEqual(len(stages), 1) + self.assertEqual(stages[0]["StageName"], "$default") diff --git a/integration/single/test_basic_layer_version.py b/integration/single/test_basic_layer_version.py new file mode 100644 index 0000000000..e5f2421f41 --- /dev/null +++ b/integration/single/test_basic_layer_version.py @@ -0,0 +1,48 @@ +from unittest.case import skipIf + +from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support + + +class TestBasicLayerVersion(BaseTest): + """ + Basic AWS::Lambda::LayerVersion tests + """ + + @skipIf(current_region_does_not_support(["Layers"]), "Layers is not supported in this testing region") + def test_basic_layer_version(self): + """ + Creates a basic lambda layer version + """ + self.create_and_verify_stack("basic_layer") + + layer_logical_id_1 = self.get_logical_id_by_type("AWS::Lambda::LayerVersion") + + self.set_template_resource_property("MyLayerVersion", "Description", "A basic layer") + self.transform_template() + self.deploy_stack() + + layer_logical_id_2 = self.get_logical_id_by_type("AWS::Lambda::LayerVersion") + + self.assertFalse(layer_logical_id_1 == layer_logical_id_2) + + @skipIf(current_region_does_not_support(["Layers"]), "Layers is not supported in this testing region") + def test_basic_layer_with_parameters(self): + """ + Creates a basic lambda layer version with parameters + """ + self.create_and_verify_stack("basic_layer_with_parameters") + + outputs = self.get_stack_outputs() + layer_arn = outputs["MyLayerArn"] + license = outputs["License"] + layer_name = outputs["LayerName"] + description = outputs["Description"] + + layer_version_result = self.client_provider.lambda_client.get_layer_version_by_arn(Arn=layer_arn) + self.client_provider.lambda_client.delete_layer_version( + LayerName=layer_name, VersionNumber=layer_version_result["Version"] + ) + + self.assertEqual(layer_version_result["LicenseInfo"], license) + self.assertEqual(layer_version_result["Description"], description) diff --git a/integration/single/test_basic_state_machine.py b/integration/single/test_basic_state_machine.py new file mode 100644 index 0000000000..d5ff568222 --- /dev/null +++ b/integration/single/test_basic_state_machine.py @@ -0,0 +1,47 @@ +from unittest.case import skipIf + +from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support + + +class TestBasicLayerVersion(BaseTest): + """ + Basic AWS::Serverless::StateMachine tests + """ + + def test_basic_state_machine_inline_definition(self): + """ + Creates a State Machine from inline definition + """ + self.create_and_verify_stack("basic_state_machine_inline_definition") + + @skipIf(current_region_does_not_support(["XRay"]), "XRay is not supported in this testing region") + def test_basic_state_machine_with_tags(self): + """ + Creates a State Machine with tags + """ + self.create_and_verify_stack("basic_state_machine_with_tags") + + tags = self.get_stack_tags("MyStateMachineArn") + + self.assertIsNotNone(tags) + self._verify_tag_presence(tags, "stateMachine:createdBy", "SAM") + self._verify_tag_presence(tags, "TagOne", "ValueOne") + self._verify_tag_presence(tags, "TagTwo", "ValueTwo") + + def _verify_tag_presence(self, tags, key, value): + """ + Verifies the presence of a tag and its value + + Parameters + ---------- + tags : List of dict + List of tag objects + key : string + Tag key + value : string + Tag value + """ + tag = next(tag for tag in tags if tag["key"] == key) + self.assertIsNotNone(tag) + self.assertEqual(tag["value"], value) diff --git a/requirements/dev.txt b/requirements/dev.txt index a08e446e0d..be38513017 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -11,6 +11,11 @@ pytest~=4.6.11; python_version < '3.6' # pytest dropped python 2 support after 4 mock>=3.0.5,<4.0.0 # 4.0.0 drops Python 2 support parameterized~=0.7.4 +# Integration tests +pathlib2>=2.3.5; python_version < '3' +click~=7.1 +dateparser~=0.7 + # Requirements for examples requests~=2.24.0 diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index 4b449f5b1f..cf51ab4592 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.34.0" +__version__ = "1.35.0" diff --git a/samtranslator/feature_toggle/feature_toggle.py b/samtranslator/feature_toggle/feature_toggle.py index 510966856b..6e665390c0 100644 --- a/samtranslator/feature_toggle/feature_toggle.py +++ b/samtranslator/feature_toggle/feature_toggle.py @@ -4,6 +4,8 @@ import boto3 import logging +from botocore.config import Config + my_path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, my_path + "/..") @@ -105,8 +107,12 @@ class FeatureToggleAppConfigConfigProvider(FeatureToggleConfigProvider): def __init__(self, application_id, environment_id, configuration_profile_id): FeatureToggleConfigProvider.__init__(self) - self.app_config_client = boto3.client("appconfig") try: + LOG.info("Loading feature toggle config from AppConfig...") + # Lambda function has 120 seconds limit + # (5 + 25) * 2, 60 seconds maximum timeout duration + client_config = Config(connect_timeout=5, read_timeout=25, retries={"total_max_attempts": 2}) + self.app_config_client = boto3.client("appconfig", config=client_config) response = self.app_config_client.get_configuration( Application=application_id, Environment=environment_id, @@ -115,6 +121,7 @@ def __init__(self, application_id, environment_id, configuration_profile_id): ) binary_config_string = response["Content"].read() self.feature_toggle_config = json.loads(binary_config_string.decode("utf-8")) + LOG.info("Finished loading feature toggle config from AppConfig.") except Exception as ex: LOG.error("Failed to load config from AppConfig: {}. Using empty config.".format(ex)) # There is chance that AppConfig is not available in a particular region. diff --git a/samtranslator/model/api/http_api_generator.py b/samtranslator/model/api/http_api_generator.py index d9e07d5d67..e3efc07c78 100644 --- a/samtranslator/model/api/http_api_generator.py +++ b/samtranslator/model/api/http_api_generator.py @@ -14,7 +14,7 @@ from samtranslator.open_api.open_api import OpenApiEditor from samtranslator.translator import logical_id_generator from samtranslator.model.tags.resource_tagging import get_tag_list -from samtranslator.model.intrinsics import is_intrinsic +from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_no_value from samtranslator.model.route53 import Route53RecordSetGroup _CORS_WILDCARD = "*" @@ -467,6 +467,15 @@ def _set_default_authorizer(self, open_api_editor, authorizers, default_authoriz if not default_authorizer: return + if is_intrinsic_no_value(default_authorizer): + return + + if is_intrinsic(default_authorizer): + raise InvalidResourceException( + self.logical_id, + "Unable to set DefaultAuthorizer because intrinsic functions are not supported for this field.", + ) + if not authorizers.get(default_authorizer): raise InvalidResourceException( self.logical_id, diff --git a/samtranslator/model/function_policies.py b/samtranslator/model/function_policies.py index 91a5be28a1..1866cd9921 100644 --- a/samtranslator/model/function_policies.py +++ b/samtranslator/model/function_policies.py @@ -3,7 +3,12 @@ from six import string_types -from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_if, is_intrinsic_no_value +from samtranslator.model.intrinsics import ( + is_intrinsic, + is_intrinsic_if, + is_intrinsic_no_value, + validate_intrinsic_if_items, +) from samtranslator.model.exceptions import InvalidTemplateException PolicyEntry = namedtuple("PolicyEntry", "data type") @@ -165,8 +170,10 @@ def _get_type_from_intrinsic_if(self, policy): """ intrinsic_if_value = policy["Fn::If"] - if not len(intrinsic_if_value) == 3: - raise InvalidTemplateException("Fn::If requires 3 arguments") + try: + validate_intrinsic_if_items(intrinsic_if_value) + except ValueError as e: + raise InvalidTemplateException(e) if_data = intrinsic_if_value[1] else_data = intrinsic_if_value[2] diff --git a/samtranslator/model/intrinsics.py b/samtranslator/model/intrinsics.py index 57982f5148..c81e40d00a 100644 --- a/samtranslator/model/intrinsics.py +++ b/samtranslator/model/intrinsics.py @@ -162,6 +162,24 @@ def is_intrinsic_if(input): return key == "Fn::If" +def validate_intrinsic_if_items(items): + """ + Validates Fn::If items + + Parameters + ---------- + items : list + Fn::If items + + Raises + ------ + ValueError + If the items are invalid + """ + if not isinstance(items, list) or len(items) != 3: + raise ValueError("Fn::If requires 3 arguments") + + def is_intrinsic_no_value(input): """ Is the given input an intrinsic Ref: AWS::NoValue? Intrinsic function is a dictionary with single diff --git a/samtranslator/model/preferences/deployment_preference_collection.py b/samtranslator/model/preferences/deployment_preference_collection.py index a226eb766e..2387931541 100644 --- a/samtranslator/model/preferences/deployment_preference_collection.py +++ b/samtranslator/model/preferences/deployment_preference_collection.py @@ -1,8 +1,15 @@ from .deployment_preference import DeploymentPreference from samtranslator.model.codedeploy import CodeDeployApplication from samtranslator.model.codedeploy import CodeDeployDeploymentGroup +from samtranslator.model.exceptions import InvalidResourceException from samtranslator.model.iam import IAMRole -from samtranslator.model.intrinsics import fnSub, is_intrinsic +from samtranslator.model.intrinsics import ( + fnSub, + is_intrinsic, + is_intrinsic_if, + is_intrinsic_no_value, + validate_intrinsic_if_items, +) from samtranslator.model.update_policy import UpdatePolicy from samtranslator.translator.arn_generator import ArnGenerator import copy @@ -125,11 +132,10 @@ def deployment_group(self, function_logical_id): deployment_group = CodeDeployDeploymentGroup(self.deployment_group_logical_id(function_logical_id)) - if deployment_preference.alarms is not None: - deployment_group.AlarmConfiguration = { - "Enabled": True, - "Alarms": [{"Name": alarm} for alarm in deployment_preference.alarms], - } + try: + deployment_group.AlarmConfiguration = self._convert_alarms(deployment_preference.alarms) + except ValueError as e: + raise InvalidResourceException(function_logical_id, str(e)) deployment_group.ApplicationName = self.codedeploy_application.get_runtime_attr("name") deployment_group.AutoRollbackConfiguration = { @@ -152,6 +158,68 @@ def deployment_group(self, function_logical_id): return deployment_group + def _convert_alarms(self, preference_alarms): + """ + Converts deployment preference alarms to an AlarmsConfiguration + + Parameters + ---------- + preference_alarms : dict + Deployment preference alarms + + Returns + ------- + dict + AlarmsConfiguration if alarms is set, None otherwise + + Raises + ------ + ValueError + If Alarms is in the wrong format + """ + if not preference_alarms or is_intrinsic_no_value(preference_alarms): + return None + + if is_intrinsic_if(preference_alarms): + processed_alarms = copy.deepcopy(preference_alarms) + alarms_list = processed_alarms.get("Fn::If") + validate_intrinsic_if_items(alarms_list) + alarms_list[1] = self._build_alarm_configuration(alarms_list[1]) + alarms_list[2] = self._build_alarm_configuration(alarms_list[2]) + return processed_alarms + + return self._build_alarm_configuration(preference_alarms) + + def _build_alarm_configuration(self, alarms): + """ + Builds an AlarmConfiguration from a list of alarms + + Parameters + ---------- + alarms : list[str] + Alarms + + Returns + ------- + dict + AlarmsConfiguration for a deployment group + + Raises + ------ + ValueError + If alarms is not a list + """ + if not isinstance(alarms, list): + raise ValueError("Alarms must be a list") + + if len(alarms) == 0 or is_intrinsic_no_value(alarms[0]): + return {} + + return { + "Enabled": True, + "Alarms": [{"Name": alarm} for alarm in alarms], + } + def _replace_deployment_types(self, value, key=None): if isinstance(value, list): for i in range(len(value)): diff --git a/samtranslator/model/resource_policies.py b/samtranslator/model/resource_policies.py index 810eb7976e..7bf3a23b18 100644 --- a/samtranslator/model/resource_policies.py +++ b/samtranslator/model/resource_policies.py @@ -3,7 +3,12 @@ from six import string_types -from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_if, is_intrinsic_no_value +from samtranslator.model.intrinsics import ( + is_intrinsic, + is_intrinsic_if, + is_intrinsic_no_value, + validate_intrinsic_if_items, +) from samtranslator.model.exceptions import InvalidTemplateException PolicyEntry = namedtuple("PolicyEntry", "data type") @@ -165,8 +170,10 @@ def _get_type_from_intrinsic_if(self, policy): """ intrinsic_if_value = policy["Fn::If"] - if not len(intrinsic_if_value) == 3: - raise InvalidTemplateException("Fn::If requires 3 arguments") + try: + validate_intrinsic_if_items(intrinsic_if_value) + except ValueError as e: + raise InvalidTemplateException(e) if_data = intrinsic_if_value[1] else_data = intrinsic_if_value[2] diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index bac7d79ce9..a3a9a6d176 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -47,6 +47,7 @@ from samtranslator.model.sns import SNSTopic from samtranslator.model.stepfunctions import StateMachineGenerator from samtranslator.model.role_utils import construct_role_for_resource +from samtranslator.model.xray_utils import get_xray_managed_policy_name class SamFunction(SamResourceMacro): @@ -453,13 +454,7 @@ def _construct_role(self, managed_policy_map, event_invoke_policies): managed_policy_arns = [ArnGenerator.generate_aws_managed_policy_arn("service-role/AWSLambdaBasicExecutionRole")] if self.Tracing: - # use previous (old) policy name for regular regions - # for china and gov regions, use the newer policy name - partition_name = ArnGenerator.get_partition_name() - if partition_name == "aws": - managed_policy_name = "AWSXrayWriteOnlyAccess" - else: - managed_policy_name = "AWSXRayDaemonWriteAccess" + managed_policy_name = get_xray_managed_policy_name() managed_policy_arns.append(ArnGenerator.generate_aws_managed_policy_arn(managed_policy_name)) if self.VpcConfig: managed_policy_arns.append( diff --git a/samtranslator/model/stepfunctions/generators.py b/samtranslator/model/stepfunctions/generators.py index 726458086c..8e1797c7df 100644 --- a/samtranslator/model/stepfunctions/generators.py +++ b/samtranslator/model/stepfunctions/generators.py @@ -17,6 +17,7 @@ from samtranslator.model.tags.resource_tagging import get_tag_list from samtranslator.model.intrinsics import is_intrinsic +from samtranslator.model.xray_utils import get_xray_managed_policy_name from samtranslator.utils.cfn_dynamic_references import is_dynamic_reference @@ -210,8 +211,12 @@ def _construct_role(self): :returns: the generated IAM Role :rtype: model.iam.IAMRole """ + policies = self.policies[:] + if self.tracing and self.tracing.get("Enabled") is True: + policies.append(get_xray_managed_policy_name()) + state_machine_policies = ResourcePolicies( - {"Policies": self.policies}, + {"Policies": policies}, # No support for policy templates in the "core" policy_template_processor=None, ) diff --git a/samtranslator/model/xray_utils.py b/samtranslator/model/xray_utils.py new file mode 100644 index 0000000000..361433932f --- /dev/null +++ b/samtranslator/model/xray_utils.py @@ -0,0 +1,10 @@ +from samtranslator.translator.arn_generator import ArnGenerator + + +def get_xray_managed_policy_name(): + # use previous (old) policy name for regular regions + # for china and gov regions, use the newer policy name + partition_name = ArnGenerator.get_partition_name() + if partition_name == "aws": + return "AWSXrayWriteOnlyAccess" + return "AWSXRayDaemonWriteAccess" diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index 230bd86c56..fc0f77a26e 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -102,7 +102,14 @@ def get_integration_function_logical_id(self, path_name, method_name): # Extract lambda integration (${LambdaName.Arn}) and split ".Arn" off from it regex = "([A-Za-z0-9]+\.Arn)" - match = re.findall(regex, arn)[0].split(".Arn")[0] + matches = re.findall(regex, arn) + # Prevent IndexError when integration URI doesn't contain .Arn (e.g. a Function with + # AutoPublishAlias translates to AWS::Lambda::Alias, which make_shorthand represents + # as LogicalId instead of LogicalId.Arn). + # TODO: Consistent handling of Functions with and without AutoPublishAlias (see #1901) + if not matches: + return False + match = matches[0].split(".Arn")[0] return match def method_has_integration(self, method): diff --git a/samtranslator/plugins/api/implicit_http_api_plugin.py b/samtranslator/plugins/api/implicit_http_api_plugin.py index 3f8849dbc1..83c078ea15 100644 --- a/samtranslator/plugins/api/implicit_http_api_plugin.py +++ b/samtranslator/plugins/api/implicit_http_api_plugin.py @@ -57,6 +57,13 @@ def _process_api_events(self, function, api_events, template, condition=None): for logicalId, event in api_events.items(): # api_events only contains HttpApi events event_properties = event.get("Properties", {}) + + if event_properties and not isinstance(event_properties, dict): + raise InvalidEventException( + logicalId, + "Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue.", + ) + if not event_properties: event["Properties"] = event_properties self._add_implicit_api_id_if_necessary(event_properties) diff --git a/samtranslator/plugins/api/implicit_rest_api_plugin.py b/samtranslator/plugins/api/implicit_rest_api_plugin.py index 3a6461928c..92ecf8d86d 100644 --- a/samtranslator/plugins/api/implicit_rest_api_plugin.py +++ b/samtranslator/plugins/api/implicit_rest_api_plugin.py @@ -63,6 +63,12 @@ def _process_api_events(self, function, api_events, template, condition=None): if not event_properties: continue + if not isinstance(event_properties, dict): + raise InvalidEventException( + logicalId, + "Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue.", + ) + self._add_implicit_api_id_if_necessary(event_properties) api_id = self._get_api_id(event_properties) diff --git a/samtranslator/plugins/application/serverless_app_plugin.py b/samtranslator/plugins/application/serverless_app_plugin.py index c043ceeca9..2f4d9b0e4e 100644 --- a/samtranslator/plugins/application/serverless_app_plugin.py +++ b/samtranslator/plugins/application/serverless_app_plugin.py @@ -156,12 +156,14 @@ def _handle_get_application_request(self, app_id, semver, key, logical_id): :param string key: The dictionary key consisting of (ApplicationId, SemanticVersion) :param string logical_id: the logical_id of this application resource """ + LOG.info("Getting application {}/{} from serverless application repo...".format(app_id, semver)) get_application = lambda app_id, semver: self._sar_client.get_application( ApplicationId=self._sanitize_sar_str_param(app_id), SemanticVersion=self._sanitize_sar_str_param(semver) ) try: self._sar_service_call(get_application, logical_id, app_id, semver) self._applications[key] = {"Available"} + LOG.info("Finished getting application {}/{}.".format(app_id, semver)) except EndpointConnectionError as e: # No internet connection. Don't break verification, but do show a warning. warning_message = "{}. Unable to verify access to {}/{}.".format(e, app_id, semver) @@ -177,10 +179,12 @@ def _handle_create_cfn_template_request(self, app_id, semver, key, logical_id): :param string key: The dictionary key consisting of (ApplicationId, SemanticVersion) :param string logical_id: the logical_id of this application resource """ + LOG.info("Requesting to create CFN template {}/{} in serverless application repo...".format(app_id, semver)) create_cfn_template = lambda app_id, semver: self._sar_client.create_cloud_formation_template( ApplicationId=self._sanitize_sar_str_param(app_id), SemanticVersion=self._sanitize_sar_str_param(semver) ) response = self._sar_service_call(create_cfn_template, logical_id, app_id, semver) + LOG.info("Requested to create CFN template {}/{} in serverless application repo.".format(app_id, semver)) self._applications[key] = response[self.TEMPLATE_URL_KEY] if response["Status"] != "ACTIVE": self._in_progress_templates.append((response[self.APPLICATION_ID_KEY], response["TemplateId"])) @@ -293,6 +297,7 @@ def on_after_transform_template(self, template): self._in_progress_templates = [] # Check each resource to make sure it's active + LOG.info("Checking resources in serverless application repo...") for application_id, template_id in temp: get_cfn_template = ( lambda application_id, template_id: self._sar_client.get_cloud_formation_template( @@ -302,6 +307,7 @@ def on_after_transform_template(self, template): ) response = self._sar_service_call(get_cfn_template, application_id, application_id, template_id) self._handle_get_cfn_template_response(response, application_id, template_id) + LOG.info("Finished checking resources in serverless application repo.") # Don't sleep if there are no more templates with PREPARING status if len(self._in_progress_templates) == 0: diff --git a/samtranslator/policy_templates_data/policy_templates.json b/samtranslator/policy_templates_data/policy_templates.json index 3682d8f1fc..4aeabb608c 100644 --- a/samtranslator/policy_templates_data/policy_templates.json +++ b/samtranslator/policy_templates_data/policy_templates.json @@ -478,9 +478,7 @@ "Action": [ "ec2:DescribeImages" ], - "Resource": { - "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:image/*" - } + "Resource": "*" } ] } @@ -2306,6 +2304,62 @@ } ] } + }, + "AcmGetCertificatePolicy": { + "Description": "Gives permission to retrieve a certificate and its certificate chain from ACM", + "Parameters": { + "CertificateArn": { + "Description": "The ARN of the certificate to grant access to" + } + }, + "Definition": { + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "acm:GetCertificate" + ], + "Resource": { + "Fn::Sub": [ + "${certificateArn}", + { + "certificateArn": { + "Ref": "CertificateArn" + } + } + ] + } + } + ] + } + }, + "Route53ChangeResourceRecordSetsPolicy": { + "Description": "Gives permission to change resource record sets in Route 53", + "Parameters": { + "HostedZoneId": { + "Description": "ID of the hosted zone" + } + }, + "Definition": { + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "route53:ChangeResourceRecordSets" + ], + "Resource": { + "Fn::Sub": [ + "arn:${AWS::Partition}:route53:::hostedzone/${HostedZoneId}", + { + "HostedZoneId": { + "Ref": "HostedZoneId" + } + } + ] + } + } + ] + } } } } diff --git a/samtranslator/sdk/parameter.py b/samtranslator/sdk/parameter.py index 161bd95b49..251f830c1b 100644 --- a/samtranslator/sdk/parameter.py +++ b/samtranslator/sdk/parameter.py @@ -1,6 +1,8 @@ import boto3 import copy +from samtranslator.translator.arn_generator import ArnGenerator, NoRegionFound + class SamParameterValues(object): """ @@ -58,21 +60,20 @@ def add_default_parameter_values(self, sam_template): if param_name not in self.parameter_values and isinstance(value, dict) and "Default" in value: self.parameter_values[param_name] = value["Default"] - def add_pseudo_parameter_values(self): + def add_pseudo_parameter_values(self, session=None): """ Add pseudo parameter values :return: parameter values that have pseudo parameter in it """ + + if session is None: + session = boto3.session.Session() + + if not session.region_name: + raise NoRegionFound("AWS Region cannot be found") + if "AWS::Region" not in self.parameter_values: - self.parameter_values["AWS::Region"] = boto3.session.Session().region_name + self.parameter_values["AWS::Region"] = session.region_name if "AWS::Partition" not in self.parameter_values: - region = boto3.session.Session().region_name - - # neither boto nor botocore has any way of returning the partition value yet - if region.startswith("cn-"): - self.parameter_values["AWS::Partition"] = "aws-cn" - elif region.startswith("us-gov-"): - self.parameter_values["AWS::Partition"] = "aws-us-gov" - else: - self.parameter_values["AWS::Partition"] = "aws" + self.parameter_values["AWS::Partition"] = ArnGenerator.get_partition_name(session.region_name) diff --git a/samtranslator/translator/arn_generator.py b/samtranslator/translator/arn_generator.py index b325c368e9..9394a6c0ae 100644 --- a/samtranslator/translator/arn_generator.py +++ b/samtranslator/translator/arn_generator.py @@ -1,7 +1,13 @@ import boto3 +class NoRegionFound(Exception): + pass + + class ArnGenerator(object): + class_boto_session = None + @classmethod def generate_arn(cls, partition, service, resource, include_account_id=True): if not service or not resource: @@ -43,7 +49,17 @@ def get_partition_name(cls, region=None): if region is None: # Use Boto3 to get the region where code is running. This uses Boto's regular region resolution # mechanism, starting from AWS_DEFAULT_REGION environment variable. - region = boto3.session.Session().region_name + + if ArnGenerator.class_boto_session is None: + region = boto3.session.Session().region_name + else: + region = ArnGenerator.class_boto_session.region_name + + # If region is still None, then we could not find the region. This will only happen + # in the local context. When this is deployed, we will be able to find the region like + # we did before. + if region is None: + raise NoRegionFound("AWS Region cannot be found") # setting default partition to aws, this will be overwritten by checking the region below partition = "aws" diff --git a/samtranslator/translator/managed_policy_translator.py b/samtranslator/translator/managed_policy_translator.py index 8621ec21d1..0b5d1f78f0 100644 --- a/samtranslator/translator/managed_policy_translator.py +++ b/samtranslator/translator/managed_policy_translator.py @@ -1,3 +1,8 @@ +import logging + +LOG = logging.getLogger(__name__) + + class ManagedPolicyLoader(object): def __init__(self, iam_client): self._iam_client = iam_client @@ -5,6 +10,7 @@ def __init__(self, iam_client): def load(self): if self._policy_map is None: + LOG.info("Loading policies from IAM...") paginator = self._iam_client.get_paginator("list_policies") # Setting the scope to AWS limits the returned values to only AWS Managed Policies and will # not returned policies owned by any specific account. @@ -15,5 +21,6 @@ def load(self): for page in page_iterator: name_to_arn_map.update(map(lambda x: (x["PolicyName"], x["Arn"]), page["Policies"])) + LOG.info("Finished loading policies from IAM.") self._policy_map = name_to_arn_map return self._policy_map diff --git a/samtranslator/translator/translator.py b/samtranslator/translator/translator.py index 29e6974894..d4ca78068d 100644 --- a/samtranslator/translator/translator.py +++ b/samtranslator/translator/translator.py @@ -25,12 +25,13 @@ from samtranslator.plugins.policies.policy_templates_plugin import PolicyTemplatesForResourcePlugin from samtranslator.policy_template_processor.processor import PolicyTemplatesProcessor from samtranslator.sdk.parameter import SamParameterValues +from samtranslator.translator.arn_generator import ArnGenerator class Translator: """Translates SAM templates into CloudFormation templates""" - def __init__(self, managed_policy_map, sam_parser, plugins=None): + def __init__(self, managed_policy_map, sam_parser, plugins=None, boto_session=None): """ :param dict managed_policy_map: Map of managed policy names to the ARNs :param sam_parser: Instance of a SAM Parser @@ -41,6 +42,9 @@ def __init__(self, managed_policy_map, sam_parser, plugins=None): self.plugins = plugins self.sam_parser = sam_parser self.feature_toggle = None + self.boto_session = boto_session + + ArnGenerator.class_boto_session = self.boto_session def _get_function_names(self, resource_dict, intrinsics_resolver): """ @@ -92,7 +96,7 @@ def translate(self, sam_template, parameter_values, feature_toggle=None): self.redeploy_restapi_parameters = dict() sam_parameter_values = SamParameterValues(parameter_values) sam_parameter_values.add_default_parameter_values(sam_template) - sam_parameter_values.add_pseudo_parameter_values() + sam_parameter_values.add_pseudo_parameter_values(self.boto_session) parameter_values = sam_parameter_values.parameter_values # Create & Install plugins sam_plugins = prepare_plugins(self.plugins, parameter_values) @@ -152,7 +156,12 @@ def translate(self, sam_template, parameter_values, feature_toggle=None): template["Resources"].update(deployment_preference_collection.codedeploy_iam_role.to_dict()) for logical_id in deployment_preference_collection.enabled_logical_ids(): - template["Resources"].update(deployment_preference_collection.deployment_group(logical_id).to_dict()) + try: + template["Resources"].update( + deployment_preference_collection.deployment_group(logical_id).to_dict() + ) + except InvalidResourceException as e: + document_errors.append(e) # Run the after-transform plugin target try: diff --git a/tests/model/api/test_http_api_generator.py b/tests/model/api/test_http_api_generator.py index 404dd7f41b..2a9f4e97a1 100644 --- a/tests/model/api/test_http_api_generator.py +++ b/tests/model/api/test_http_api_generator.py @@ -69,6 +69,19 @@ def test_auth_missing_default_auth(self): with pytest.raises(InvalidResourceException): HttpApiGenerator(**self.kwargs)._construct_http_api() + def test_auth_intrinsic_default_auth(self): + self.kwargs["auth"] = self.authorizers + self.kwargs["auth"]["DefaultAuthorizer"] = {"Ref": "SomeValue"} + self.kwargs["definition_body"] = OpenApiEditor.gen_skeleton() + with pytest.raises(InvalidResourceException): + HttpApiGenerator(**self.kwargs)._construct_http_api() + + def test_auth_novalue_default_does_not_raise(self): + self.kwargs["auth"] = self.authorizers + self.kwargs["auth"]["DefaultAuthorizer"] = {"Ref": "AWS::NoValue"} + self.kwargs["definition_body"] = OpenApiEditor.gen_skeleton() + HttpApiGenerator(**self.kwargs)._construct_http_api() + def test_def_uri_invalid_dict(self): self.kwargs["auth"] = None self.kwargs["definition_body"] = None diff --git a/tests/model/test_function_policies.py b/tests/model/test_function_policies.py index c7d9d90414..bcbe7f1525 100644 --- a/tests/model/test_function_policies.py +++ b/tests/model/test_function_policies.py @@ -3,7 +3,6 @@ from samtranslator.model.function_policies import FunctionPolicies, PolicyTypes, PolicyEntry from samtranslator.model.exceptions import InvalidTemplateException -from samtranslator.model.intrinsics import is_intrinsic_if, is_intrinsic_no_value class TestFunctionPolicies(TestCase): @@ -271,37 +270,6 @@ def test_is_policy_template_must_return_false_without_the_processor(self): self.assertFalse(function_policies_obj._is_policy_template(policy)) self.policy_template_processor_mock.has.assert_not_called() - def test_is_intrinsic_if_must_return_true_for_if(self): - policy = {"Fn::If": "some value"} - - self.assertTrue(is_intrinsic_if(policy)) - - def test_is_intrinsic_if_must_return_false_for_others(self): - too_many_keys = {"Fn::If": "some value", "Fn::And": "other value"} - - not_if = {"Fn::Or": "some value"} - - self.assertFalse(is_intrinsic_if(too_many_keys)) - self.assertFalse(is_intrinsic_if(not_if)) - self.assertFalse(is_intrinsic_if(None)) - - def test_is_intrinsic_no_value_must_return_true_for_no_value(self): - policy = {"Ref": "AWS::NoValue"} - - self.assertTrue(is_intrinsic_no_value(policy)) - - def test_is_intrinsic_no_value_must_return_false_for_other_value(self): - bad_key = {"sRefs": "AWS::NoValue"} - - bad_value = {"Ref": "SWA::NoValue"} - - too_many_keys = {"Ref": "AWS::NoValue", "feR": "SWA::NoValue"} - - self.assertFalse(is_intrinsic_no_value(bad_key)) - self.assertFalse(is_intrinsic_no_value(bad_value)) - self.assertFalse(is_intrinsic_no_value(None)) - self.assertFalse(is_intrinsic_no_value(too_many_keys)) - def test_get_type_with_intrinsic_if_must_return_managed_policy_type(self): managed_policy = {"Fn::If": ["SomeCondition", "some managed policy arn", "other managed policy arn"]} diff --git a/tests/openapi/test_openapi.py b/tests/openapi/test_openapi.py index 9938cab1c7..f29cb4d47e 100644 --- a/tests/openapi/test_openapi.py +++ b/tests/openapi/test_openapi.py @@ -437,3 +437,50 @@ def test_must_not_add_description_if_already_defined(self): editor = OpenApiEditor(self.original_openapi_with_description) editor.add_description("New Description") self.assertEqual(editor.openapi["info"]["description"], "Existing Description") + + +class TestOpenApiEditor_get_integration_function_of_alias(TestCase): + def setUp(self): + + self.original_openapi = { + "openapi": "3.0.1", + "paths": { + "$default": { + "x-amazon-apigateway-any-method": { + "Fn::If": [ + "condition", + { + "security": [{"OpenIdAuth": ["scope1", "scope2"]}], + "isDefaultRoute": True, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::If": [ + "condition", + { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HttpApiFunctionAlias}/invocations" + }, + {"Ref": "AWS::NoValue"}, + ] + }, + "payloadFormatVersion": "1.0", + }, + "responses": {}, + }, + {"Ref": "AWS::NoValue"}, + ] + } + }, + "/bar": {}, + "/badpath": "string value", + }, + } + + self.editor = OpenApiEditor(self.original_openapi) + + def test_no_logical_id_if_alias(self): + + self.assertFalse( + self.editor.get_integration_function_logical_id(OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD), + ) diff --git a/tests/sdk/test_parameter.py b/tests/sdk/test_parameter.py index d69aa77b2b..afcc3ccc1d 100644 --- a/tests/sdk/test_parameter.py +++ b/tests/sdk/test_parameter.py @@ -1,9 +1,10 @@ from parameterized import parameterized, param -import pytest from unittest import TestCase from samtranslator.sdk.parameter import SamParameterValues -from mock import patch +from mock import patch, Mock + +from samtranslator.translator.arn_generator import NoRegionFound class TestSAMParameterValues(TestCase): @@ -101,3 +102,10 @@ def test_add_pseudo_parameter_values_aws_partition_not_override(self): sam_parameter_values = SamParameterValues(parameter_values) sam_parameter_values.add_pseudo_parameter_values() self.assertEqual(expected, sam_parameter_values.parameter_values) + + def test_add_pseudo_parameter_values_raises_NoRegionFound(self): + boto_session_mock = Mock() + boto_session_mock.region_name = None + sam_parameter_values = SamParameterValues({}) + with self.assertRaises(NoRegionFound): + sam_parameter_values.add_pseudo_parameter_values(session=boto_session_mock) diff --git a/tests/test_intrinsics.py b/tests/test_intrinsics.py index 5a3f22f4f5..4de2e82de4 100644 --- a/tests/test_intrinsics.py +++ b/tests/test_intrinsics.py @@ -1,7 +1,13 @@ from parameterized import parameterized from unittest import TestCase -from samtranslator.model.intrinsics import is_intrinsic, make_shorthand +from samtranslator.model.intrinsics import ( + is_intrinsic, + make_shorthand, + is_intrinsic_if, + validate_intrinsic_if_items, + is_intrinsic_no_value, +) class TestIntrinsics(TestCase): @@ -30,3 +36,48 @@ def test_make_short_hand_failure(self): with self.assertRaises(NotImplementedError): make_shorthand(input) + + def test_is_intrinsic_no_value_must_return_true_for_no_value(self): + policy = {"Ref": "AWS::NoValue"} + + self.assertTrue(is_intrinsic_no_value(policy)) + + def test_is_intrinsic_no_value_must_return_false_for_other_value(self): + bad_key = {"sRefs": "AWS::NoValue"} + bad_value = {"Ref": "SWA::NoValue"} + too_many_keys = {"Ref": "AWS::NoValue", "feR": "SWA::NoValue"} + + self.assertFalse(is_intrinsic_no_value(bad_key)) + self.assertFalse(is_intrinsic_no_value(bad_value)) + self.assertFalse(is_intrinsic_no_value(None)) + self.assertFalse(is_intrinsic_no_value(too_many_keys)) + + def test_is_intrinsic_if_must_return_true_for_if(self): + policy = {"Fn::If": "some value"} + + self.assertTrue(is_intrinsic_if(policy)) + + def test_is_intrinsic_if_must_return_false_for_others(self): + too_many_keys = {"Fn::If": "some value", "Fn::And": "other value"} + not_if = {"Fn::Or": "some value"} + + self.assertFalse(is_intrinsic_if(too_many_keys)) + self.assertFalse(is_intrinsic_if(not_if)) + self.assertFalse(is_intrinsic_if(None)) + + def test_validate_intrinsic_if_items_valid(self): + validate_intrinsic_if_items(["Condition", "Then", "Else"]) + + def test_validate_intrinsic_if_items_invalid(self): + not_enough_items = ["Then", "Else"] + is_string = "Then" + is_integer = 3 + is_boolean = True + is_dict = {"Fn::If": "some value", "Fn::And": "other value"} + + self.assertRaises(ValueError, validate_intrinsic_if_items, not_enough_items) + self.assertRaises(ValueError, validate_intrinsic_if_items, is_string) + self.assertRaises(ValueError, validate_intrinsic_if_items, None) + self.assertRaises(ValueError, validate_intrinsic_if_items, is_integer) + self.assertRaises(ValueError, validate_intrinsic_if_items, is_boolean) + self.assertRaises(ValueError, validate_intrinsic_if_items, is_dict) diff --git a/tests/translator/input/all_policy_templates.yaml b/tests/translator/input/all_policy_templates.yaml index 5216a65f2c..c46cfaecb9 100644 --- a/tests/translator/input/all_policy_templates.yaml +++ b/tests/translator/input/all_policy_templates.yaml @@ -168,3 +168,9 @@ Resources: - EventBridgePutEventsPolicy: EventBusName: name + + - AcmGetCertificatePolicy: + CertificateArn: arn + + - Route53ChangeResourceRecordSetsPolicy: + HostedZoneId: test diff --git a/tests/translator/input/error_function_invalid_api_event.yaml b/tests/translator/input/error_function_invalid_api_event.yaml index 98b7aa5f01..03a98c2a04 100644 --- a/tests/translator/input/error_function_invalid_api_event.yaml +++ b/tests/translator/input/error_function_invalid_api_event.yaml @@ -1,4 +1,15 @@ Resources: + FunctionApiInvalidProperties: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Events: + ApiEvent: + Type: Api + Properties: '/hello' # NOTE: Should be an object no a string + FunctionApiNoMethod: Type: 'AWS::Serverless::Function' Properties: diff --git a/tests/translator/input/error_function_with_deployment_preference_invalid_alarms.yaml b/tests/translator/input/error_function_with_deployment_preference_invalid_alarms.yaml new file mode 100644 index 0000000000..1ef2d22d39 --- /dev/null +++ b/tests/translator/input/error_function_with_deployment_preference_invalid_alarms.yaml @@ -0,0 +1,16 @@ +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: Linear10PercentEvery3Minutes + Alarms: MyAlarm diff --git a/tests/translator/input/error_implicit_http_api_properties.yaml b/tests/translator/input/error_implicit_http_api_properties.yaml new file mode 100644 index 0000000000..33770f00d1 --- /dev/null +++ b/tests/translator/input/error_implicit_http_api_properties.yaml @@ -0,0 +1,12 @@ +Resources: + HttpApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.restapi + Runtime: nodejs12.x + Policies: AmazonDynamoDBFullAccess + Events: + Basic: + Type: HttpApi + Properties: /basic diff --git a/tests/translator/input/function_with_deployment_preference_alarms_intrinsic_if.yaml b/tests/translator/input/function_with_deployment_preference_alarms_intrinsic_if.yaml new file mode 100644 index 0000000000..f392f10628 --- /dev/null +++ b/tests/translator/input/function_with_deployment_preference_alarms_intrinsic_if.yaml @@ -0,0 +1,23 @@ +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: Linear10PercentEvery3Minutes + Alarms: + Fn::If: + - MyCondition + - - Alarm1 + - Alarm2 + - Alarm3 + - - Alarm1 + - Alarm5 diff --git a/tests/translator/input/state_machine_with_xray_policies.yaml b/tests/translator/input/state_machine_with_xray_policies.yaml new file mode 100644 index 0000000000..719d5874ab --- /dev/null +++ b/tests/translator/input/state_machine_with_xray_policies.yaml @@ -0,0 +1,22 @@ +Resources: + MyFunction: + Type: "AWS::Serverless::Function" + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + + StateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Name: MyBasicStateMachine + Type: STANDARD + DefinitionUri: s3://sam-demo-bucket/my-state-machine.asl.json + Tracing: + Enabled: true + Policies: + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: lambda:InvokeFunction + Resource: !GetAtt MyFunction.Arn diff --git a/tests/translator/input/state_machine_with_xray.yaml b/tests/translator/input/state_machine_with_xray_role.yaml similarity index 100% rename from tests/translator/input/state_machine_with_xray.yaml rename to tests/translator/input/state_machine_with_xray_role.yaml diff --git a/tests/translator/model/preferences/test_deployment_preference_collection.py b/tests/translator/model/preferences/test_deployment_preference_collection.py index ae8b39bdc8..1d9b14b2d1 100644 --- a/tests/translator/model/preferences/test_deployment_preference_collection.py +++ b/tests/translator/model/preferences/test_deployment_preference_collection.py @@ -163,6 +163,170 @@ def test_deployment_group_with_all_parameters(self): self.assertEqual(deployment_group.to_dict(), expected_deployment_group.to_dict()) + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": ["Alarm1", "Alarm2", "Alarm3"], + } + expected_alarm_configuration = { + "Enabled": True, + "Alarms": [{"Name": "Alarm1"}, {"Name": "Alarm2"}, {"Name": "Alarm3"}], + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id) + + self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_intrinsic_if(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": {"Fn::If": ["MyCondition", ["Alarm1", "Alarm2"], ["Alarm3"]]}, + } + expected_alarm_configuration = { + "Fn::If": [ + "MyCondition", + {"Enabled": True, "Alarms": [{"Name": "Alarm1"}, {"Name": "Alarm2"}]}, + {"Enabled": True, "Alarms": [{"Name": "Alarm3"}]}, + ], + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id) + + self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_intrinsic_if_empty_then(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": {"Fn::If": ["MyCondition", [], ["Alarm3"]]}, + } + expected_alarm_configuration = { + "Fn::If": ["MyCondition", {}, {"Enabled": True, "Alarms": [{"Name": "Alarm3"}]}], + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id) + + self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_intrinsic_if_noref_then(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": {"Fn::If": ["MyCondition", [{"Ref": "AWS::NoValue"}], ["Alarm3"]]}, + } + expected_alarm_configuration = { + "Fn::If": ["MyCondition", {}, {"Enabled": True, "Alarms": [{"Name": "Alarm3"}]}], + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id) + + self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_intrinsic_if_empty_else(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": {"Fn::If": ["MyCondition", ["Alarm1", "Alarm2"], []]}, + } + expected_alarm_configuration = { + "Fn::If": ["MyCondition", {"Enabled": True, "Alarms": [{"Name": "Alarm1"}, {"Name": "Alarm2"}]}, {}], + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id) + + self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_intrinsic_if_noref_else(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": {"Fn::If": ["MyCondition", ["Alarm1", "Alarm2"], [{"Ref": "AWS::NoValue"}]]}, + } + expected_alarm_configuration = { + "Fn::If": ["MyCondition", {"Enabled": True, "Alarms": [{"Name": "Alarm1"}, {"Name": "Alarm2"}]}, {}], + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id) + + self.assertEqual(expected_alarm_configuration, deployment_group.AlarmConfiguration) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_ref_novalue(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": {"Ref": "AWS::NoValue"}, + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id) + + self.assertIsNone(deployment_group.AlarmConfiguration) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_empty(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": [], + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + deployment_group = deployment_preference_collection.deployment_group(self.function_logical_id) + + self.assertIsNone(deployment_group.AlarmConfiguration) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_not_list(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": "Alarm1", + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + with self.assertRaises(InvalidResourceException) as e: + deployment_preference_collection.deployment_group(self.function_logical_id) + self.assertEqual( + e.exception.message, + "Resource with id [{}] is invalid. Alarms must be a list".format(self.function_logical_id), + ) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_intrinsic_if_missing_arg(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": {"Fn::If": ["MyCondition", ["Alarm1", "Alarm2"]]}, + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + with self.assertRaises(InvalidResourceException) as e: + deployment_preference_collection.deployment_group(self.function_logical_id) + self.assertEqual( + e.exception.message, + "Resource with id [{}] is invalid. Fn::If requires 3 arguments".format(self.function_logical_id), + ) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + def test_deployment_preference_with_alarms_intrinsic_if_not_list(self): + deployment_preference = { + "Type": "TestDeploymentConfiguration", + "Alarms": {"Fn::If": "Alarm1"}, + } + deployment_preference_collection = DeploymentPreferenceCollection() + deployment_preference_collection.add(self.function_logical_id, deployment_preference) + with self.assertRaises(InvalidResourceException) as e: + deployment_preference_collection.deployment_group(self.function_logical_id) + self.assertEqual( + e.exception.message, + "Resource with id [{}] is invalid. Fn::If requires 3 arguments".format(self.function_logical_id), + ) + @patch("boto3.session.Session.region_name", "ap-southeast-1") def test_update_policy_with_minimal_parameters(self): expected_update_policy = { diff --git a/tests/translator/output/all_policy_templates.json b/tests/translator/output/all_policy_templates.json index f98f0fbc16..a751bff434 100644 --- a/tests/translator/output/all_policy_templates.json +++ b/tests/translator/output/all_policy_templates.json @@ -336,9 +336,7 @@ "Action": [ "ec2:DescribeImages" ], - "Resource": { - "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:image/*" - }, + "Resource": "*", "Effect": "Allow" } ] @@ -1570,6 +1568,48 @@ } ] } + }, + { + "PolicyName": "KitchenSinkFunctionRolePolicy58", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "acm:GetCertificate" + ], + "Resource": { + "Fn::Sub": [ + "${certificateArn}", + { + "certificateArn": "arn" + } + ] + }, + "Effect": "Allow" + } + ] + } + }, + { + "PolicyName": "KitchenSinkFunctionRolePolicy59", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "route53:ChangeResourceRecordSets" + ], + "Resource": { + "Fn::Sub": [ + "arn:${AWS::Partition}:route53:::hostedzone/${HostedZoneId}", + { + "HostedZoneId": "test" + } + ] + }, + "Effect": "Allow" + } + ] + } } ], "Tags": [ diff --git a/tests/translator/output/aws-cn/all_policy_templates.json b/tests/translator/output/aws-cn/all_policy_templates.json index fdd861115b..7ba527e40c 100644 --- a/tests/translator/output/aws-cn/all_policy_templates.json +++ b/tests/translator/output/aws-cn/all_policy_templates.json @@ -336,9 +336,7 @@ "Action": [ "ec2:DescribeImages" ], - "Resource": { - "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:image/*" - }, + "Resource": "*", "Effect": "Allow" } ] @@ -1570,6 +1568,48 @@ } ] } + }, + { + "PolicyName": "KitchenSinkFunctionRolePolicy58", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "acm:GetCertificate" + ], + "Resource": { + "Fn::Sub": [ + "${certificateArn}", + { + "certificateArn": "arn" + } + ] + }, + "Effect": "Allow" + } + ] + } + }, + { + "PolicyName": "KitchenSinkFunctionRolePolicy59", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "route53:ChangeResourceRecordSets" + ], + "Resource": { + "Fn::Sub": [ + "arn:${AWS::Partition}:route53:::hostedzone/${HostedZoneId}", + { + "HostedZoneId": "test" + } + ] + }, + "Effect": "Allow" + } + ] + } } ], "Tags": [ diff --git a/tests/translator/output/aws-cn/function_with_deployment_preference_alarms_intrinsic_if.json b/tests/translator/output/aws-cn/function_with_deployment_preference_alarms_intrinsic_if.json new file mode 100644 index 0000000000..077d23fc8e --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_deployment_preference_alarms_intrinsic_if.json @@ -0,0 +1,193 @@ +{ + "Conditions": { + "MyCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunctionVersion640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "MinimalFunction" + } + } + }, + "MinimalFunctionAliaslive": { + "Type": "AWS::Lambda::Alias", + "UpdatePolicy": { + "CodeDeployLambdaAliasUpdate": { + "ApplicationName": { + "Ref": "ServerlessDeploymentApplication" + }, + "DeploymentGroupName": { + "Ref": "MinimalFunctionDeploymentGroup" + } + } + }, + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunctionVersion640128d35d", + "Version" + ] + } + } + }, + "MinimalFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ServerlessDeploymentApplication": { + "Type": "AWS::CodeDeploy::Application", + "Properties": { + "ComputePlatform": "Lambda" + } + }, + "CodeDeployServiceRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "codedeploy.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda" + ] + } + }, + "MinimalFunctionDeploymentGroup": { + "Type": "AWS::CodeDeploy::DeploymentGroup", + "Properties": { + "AlarmConfiguration": { + "Fn::If": [ + "MyCondition", + { + "Enabled": true, + "Alarms": [ + { + "Name": "Alarm1" + }, + { + "Name": "Alarm2" + }, + { + "Name": "Alarm3" + } + ] + }, + { + "Enabled": true, + "Alarms": [ + { + "Name": "Alarm1" + }, + { + "Name": "Alarm5" + } + ] + } + ] + }, + "ApplicationName": { + "Ref": "ServerlessDeploymentApplication" + }, + "AutoRollbackConfiguration": { + "Enabled": true, + "Events": [ + "DEPLOYMENT_FAILURE", + "DEPLOYMENT_STOP_ON_ALARM", + "DEPLOYMENT_STOP_ON_REQUEST" + ] + }, + "DeploymentConfigName": { + "Fn::Sub": [ + "CodeDeployDefault.Lambda${ConfigName}", + { + "ConfigName": "Linear10PercentEvery3Minutes" + } + ] + }, + "DeploymentStyle": { + "DeploymentType": "BLUE_GREEN", + "DeploymentOption": "WITH_TRAFFIC_CONTROL" + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "CodeDeployServiceRole", + "Arn" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/state_machine_with_xray_policies.json b/tests/translator/output/aws-cn/state_machine_with_xray_policies.json new file mode 100644 index 0000000000..c073afdc68 --- /dev/null +++ b/tests/translator/output/aws-cn/state_machine_with_xray_policies.json @@ -0,0 +1,133 @@ +{ + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "StateMachine": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionS3Location": { + "Bucket": "sam-demo-bucket", + "Key": "my-state-machine.asl.json" + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRole", + "Arn" + ] + }, + "StateMachineName": "MyBasicStateMachine", + "StateMachineType": "STANDARD", + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ], + "TracingConfiguration": { + "Enabled": true + } + } + }, + "StateMachineRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "states.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/AWSXRayDaemonWriteAccess" + ], + "Policies": [ + { + "PolicyName": "StateMachineRolePolicy0", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "lambda:InvokeFunction", + "Resource": { + "Fn::GetAtt": [ + "MyFunction", + "Arn" + ] + } + } + ] + } + } + ], + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/state_machine_with_xray.json b/tests/translator/output/aws-cn/state_machine_with_xray_role.json similarity index 100% rename from tests/translator/output/aws-cn/state_machine_with_xray.json rename to tests/translator/output/aws-cn/state_machine_with_xray_role.json diff --git a/tests/translator/output/aws-us-gov/all_policy_templates.json b/tests/translator/output/aws-us-gov/all_policy_templates.json index 9f9ac33a79..61d463baf1 100644 --- a/tests/translator/output/aws-us-gov/all_policy_templates.json +++ b/tests/translator/output/aws-us-gov/all_policy_templates.json @@ -336,9 +336,7 @@ "Action": [ "ec2:DescribeImages" ], - "Resource": { - "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:image/*" - }, + "Resource": "*", "Effect": "Allow" } ] @@ -1570,6 +1568,48 @@ } ] } + }, + { + "PolicyName": "KitchenSinkFunctionRolePolicy58", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "acm:GetCertificate" + ], + "Resource": { + "Fn::Sub": [ + "${certificateArn}", + { + "certificateArn": "arn" + } + ] + }, + "Effect": "Allow" + } + ] + } + }, + { + "PolicyName": "KitchenSinkFunctionRolePolicy59", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "route53:ChangeResourceRecordSets" + ], + "Resource": { + "Fn::Sub": [ + "arn:${AWS::Partition}:route53:::hostedzone/${HostedZoneId}", + { + "HostedZoneId": "test" + } + ] + }, + "Effect": "Allow" + } + ] + } } ], "Tags": [ diff --git a/tests/translator/output/aws-us-gov/function_with_deployment_preference_alarms_intrinsic_if.json b/tests/translator/output/aws-us-gov/function_with_deployment_preference_alarms_intrinsic_if.json new file mode 100644 index 0000000000..b156bf3184 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_deployment_preference_alarms_intrinsic_if.json @@ -0,0 +1,193 @@ +{ + "Conditions": { + "MyCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunctionVersion640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "MinimalFunction" + } + } + }, + "MinimalFunctionAliaslive": { + "Type": "AWS::Lambda::Alias", + "UpdatePolicy": { + "CodeDeployLambdaAliasUpdate": { + "ApplicationName": { + "Ref": "ServerlessDeploymentApplication" + }, + "DeploymentGroupName": { + "Ref": "MinimalFunctionDeploymentGroup" + } + } + }, + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunctionVersion640128d35d", + "Version" + ] + } + } + }, + "MinimalFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ServerlessDeploymentApplication": { + "Type": "AWS::CodeDeploy::Application", + "Properties": { + "ComputePlatform": "Lambda" + } + }, + "CodeDeployServiceRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "codedeploy.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda" + ] + } + }, + "MinimalFunctionDeploymentGroup": { + "Type": "AWS::CodeDeploy::DeploymentGroup", + "Properties": { + "AlarmConfiguration": { + "Fn::If": [ + "MyCondition", + { + "Enabled": true, + "Alarms": [ + { + "Name": "Alarm1" + }, + { + "Name": "Alarm2" + }, + { + "Name": "Alarm3" + } + ] + }, + { + "Enabled": true, + "Alarms": [ + { + "Name": "Alarm1" + }, + { + "Name": "Alarm5" + } + ] + } + ] + }, + "ApplicationName": { + "Ref": "ServerlessDeploymentApplication" + }, + "AutoRollbackConfiguration": { + "Enabled": true, + "Events": [ + "DEPLOYMENT_FAILURE", + "DEPLOYMENT_STOP_ON_ALARM", + "DEPLOYMENT_STOP_ON_REQUEST" + ] + }, + "DeploymentConfigName": { + "Fn::Sub": [ + "CodeDeployDefault.Lambda${ConfigName}", + { + "ConfigName": "Linear10PercentEvery3Minutes" + } + ] + }, + "DeploymentStyle": { + "DeploymentType": "BLUE_GREEN", + "DeploymentOption": "WITH_TRAFFIC_CONTROL" + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "CodeDeployServiceRole", + "Arn" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/state_machine_with_xray_policies.json b/tests/translator/output/aws-us-gov/state_machine_with_xray_policies.json new file mode 100644 index 0000000000..287d1b6265 --- /dev/null +++ b/tests/translator/output/aws-us-gov/state_machine_with_xray_policies.json @@ -0,0 +1,133 @@ +{ + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "StateMachine": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionS3Location": { + "Bucket": "sam-demo-bucket", + "Key": "my-state-machine.asl.json" + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRole", + "Arn" + ] + }, + "StateMachineName": "MyBasicStateMachine", + "StateMachineType": "STANDARD", + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ], + "TracingConfiguration": { + "Enabled": true + } + } + }, + "StateMachineRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "states.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/AWSXRayDaemonWriteAccess" + ], + "Policies": [ + { + "PolicyName": "StateMachineRolePolicy0", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "lambda:InvokeFunction", + "Resource": { + "Fn::GetAtt": [ + "MyFunction", + "Arn" + ] + } + } + ] + } + } + ], + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/state_machine_with_xray.json b/tests/translator/output/aws-us-gov/state_machine_with_xray_role.json similarity index 100% rename from tests/translator/output/aws-us-gov/state_machine_with_xray.json rename to tests/translator/output/aws-us-gov/state_machine_with_xray_role.json diff --git a/tests/translator/output/error_function_invalid_api_event.json b/tests/translator/output/error_function_invalid_api_event.json index 01b123ed8e..48027216a9 100644 --- a/tests/translator/output/error_function_invalid_api_event.json +++ b/tests/translator/output/error_function_invalid_api_event.json @@ -4,5 +4,5 @@ "errorMessage": "Resource with id [FunctionApiNoMethod] is invalid. Event with id [ApiEvent] is invalid. Event is missing key 'Path'." } ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 4. Resource with id [FunctionApiMethodArray] is invalid. Event with id [ApiEvent] is invalid. Api Event must have a String specified for 'Method'. Resource with id [FunctionApiNoMethod] is invalid. Event with id [ApiEvent] is invalid. Event is missing key 'Method'. Resource with id [FunctionApiNoPath] is invalid. Event with id [ApiEvent] is invalid. Event is missing key 'Path'. Resource with id [FunctionApiPathArray] is invalid. Event with id [ApiEvent] is invalid. Api Event must have a String specified for 'Path'." -} \ No newline at end of file + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 5. Resource with id [FunctionApiInvalidProperties] is invalid. Event with id [ApiEvent] is invalid. Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue. Resource with id [FunctionApiMethodArray] is invalid. Event with id [ApiEvent] is invalid. Api Event must have a String specified for 'Method'. Resource with id [FunctionApiNoMethod] is invalid. Event with id [ApiEvent] is invalid. Event is missing key 'Method'. Resource with id [FunctionApiNoPath] is invalid. Event with id [ApiEvent] is invalid. Event is missing key 'Path'. Resource with id [FunctionApiPathArray] is invalid. Event with id [ApiEvent] is invalid. Api Event must have a String specified for 'Path'." +} diff --git a/tests/translator/output/error_function_with_deployment_preference_invalid_alarms.json b/tests/translator/output/error_function_with_deployment_preference_invalid_alarms.json new file mode 100644 index 0000000000..ee4e8d6174 --- /dev/null +++ b/tests/translator/output/error_function_with_deployment_preference_invalid_alarms.json @@ -0,0 +1,11 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MinimalFunction] is invalid. Alarms must be a list" + }, + { + "errorMessage": "Resource with id [MinimalFunction] is invalid. Alarms must be a list" + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 2. Resource with id [MinimalFunction] is invalid. Alarms must be a list Resource with id [MinimalFunction] is invalid. Alarms must be a list" +} \ No newline at end of file diff --git a/tests/translator/output/error_implicit_http_api_properties.json b/tests/translator/output/error_implicit_http_api_properties.json new file mode 100644 index 0000000000..833d80e03f --- /dev/null +++ b/tests/translator/output/error_implicit_http_api_properties.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [HttpApiFunction] is invalid. Event with id [Basic] is invalid. Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HttpApiFunction] is invalid. Event with id [Basic] is invalid. Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue." +} diff --git a/tests/translator/output/function_with_deployment_preference_alarms_intrinsic_if.json b/tests/translator/output/function_with_deployment_preference_alarms_intrinsic_if.json new file mode 100644 index 0000000000..72643d9ca5 --- /dev/null +++ b/tests/translator/output/function_with_deployment_preference_alarms_intrinsic_if.json @@ -0,0 +1,193 @@ +{ + "Conditions": { + "MyCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunctionVersion640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "FunctionName": { + "Ref": "MinimalFunction" + } + } + }, + "MinimalFunctionAliaslive": { + "Type": "AWS::Lambda::Alias", + "UpdatePolicy": { + "CodeDeployLambdaAliasUpdate": { + "ApplicationName": { + "Ref": "ServerlessDeploymentApplication" + }, + "DeploymentGroupName": { + "Ref": "MinimalFunctionDeploymentGroup" + } + } + }, + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunctionVersion640128d35d", + "Version" + ] + } + } + }, + "MinimalFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ServerlessDeploymentApplication": { + "Type": "AWS::CodeDeploy::Application", + "Properties": { + "ComputePlatform": "Lambda" + } + }, + "CodeDeployServiceRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "codedeploy.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda" + ] + } + }, + "MinimalFunctionDeploymentGroup": { + "Type": "AWS::CodeDeploy::DeploymentGroup", + "Properties": { + "AlarmConfiguration": { + "Fn::If": [ + "MyCondition", + { + "Enabled": true, + "Alarms": [ + { + "Name": "Alarm1" + }, + { + "Name": "Alarm2" + }, + { + "Name": "Alarm3" + } + ] + }, + { + "Enabled": true, + "Alarms": [ + { + "Name": "Alarm1" + }, + { + "Name": "Alarm5" + } + ] + } + ] + }, + "ApplicationName": { + "Ref": "ServerlessDeploymentApplication" + }, + "AutoRollbackConfiguration": { + "Enabled": true, + "Events": [ + "DEPLOYMENT_FAILURE", + "DEPLOYMENT_STOP_ON_ALARM", + "DEPLOYMENT_STOP_ON_REQUEST" + ] + }, + "DeploymentConfigName": { + "Fn::Sub": [ + "CodeDeployDefault.Lambda${ConfigName}", + { + "ConfigName": "Linear10PercentEvery3Minutes" + } + ] + }, + "DeploymentStyle": { + "DeploymentType": "BLUE_GREEN", + "DeploymentOption": "WITH_TRAFFIC_CONTROL" + }, + "ServiceRoleArn": { + "Fn::GetAtt": [ + "CodeDeployServiceRole", + "Arn" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/state_machine_with_xray_policies.json b/tests/translator/output/state_machine_with_xray_policies.json new file mode 100644 index 0000000000..4d23fc62d4 --- /dev/null +++ b/tests/translator/output/state_machine_with_xray_policies.json @@ -0,0 +1,133 @@ +{ + "Resources": { + "MyFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "StateMachine": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionS3Location": { + "Bucket": "sam-demo-bucket", + "Key": "my-state-machine.asl.json" + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRole", + "Arn" + ] + }, + "StateMachineName": "MyBasicStateMachine", + "StateMachineType": "STANDARD", + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ], + "TracingConfiguration": { + "Enabled": true + } + } + }, + "StateMachineRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "states.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess" + ], + "Policies": [ + { + "PolicyName": "StateMachineRolePolicy0", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "lambda:InvokeFunction", + "Resource": { + "Fn::GetAtt": [ + "MyFunction", + "Arn" + ] + } + } + ] + } + } + ], + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/state_machine_with_xray.json b/tests/translator/output/state_machine_with_xray_role.json similarity index 100% rename from tests/translator/output/state_machine_with_xray.json rename to tests/translator/output/state_machine_with_xray_role.json diff --git a/tests/translator/test_arn_generator.py b/tests/translator/test_arn_generator.py new file mode 100644 index 0000000000..461840cdf4 --- /dev/null +++ b/tests/translator/test_arn_generator.py @@ -0,0 +1,35 @@ +from unittest import TestCase +from parameterized import parameterized +from mock import Mock, patch + +from samtranslator.translator.arn_generator import ArnGenerator, NoRegionFound + + +class TestArnGenerator(TestCase): + def setUp(self): + ArnGenerator.class_boto_session = None + + @parameterized.expand( + [("us-east-1", "aws"), ("cn-east-1", "aws-cn"), ("us-gov-west-1", "aws-us-gov"), ("US-EAST-1", "aws")] + ) + def test_get_partition_name(self, region, expected): + actual = ArnGenerator.get_partition_name(region) + + self.assertEqual(actual, expected) + + @patch("boto3.session.Session.region_name", None) + def test_get_partition_name_raise_NoRegionFound(self): + with self.assertRaises(NoRegionFound): + ArnGenerator.get_partition_name(None) + + def test_get_partition_name_from_boto_session(self): + boto_session_mock = Mock() + boto_session_mock.region_name = "us-east-1" + + ArnGenerator.class_boto_session = boto_session_mock + + actual = ArnGenerator.get_partition_name() + + self.assertEqual(actual, "aws") + + ArnGenerator.class_boto_session = None diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index f380d34c39..05948913f3 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -240,6 +240,7 @@ class TestTranslatorEndToEnd(TestCase): "function_with_deployment_preference_all_parameters", "function_with_deployment_preference_from_parameters", "function_with_deployment_preference_multiple_combinations", + "function_with_deployment_preference_alarms_intrinsic_if", "function_with_alias_and_event_sources", "function_with_resource_refs", "function_with_deployment_and_custom_role", @@ -310,7 +311,8 @@ class TestTranslatorEndToEnd(TestCase): "state_machine_with_api_resource_policy", "state_machine_with_api_auth_default_scopes", "state_machine_with_condition_and_events", - "state_machine_with_xray", + "state_machine_with_xray_policies", + "state_machine_with_xray_role", "function_with_file_system_config", "state_machine_with_permissions_boundary", ], @@ -348,6 +350,14 @@ def test_transform_success(self, testcase, partition_with_region): "AmazonDynamoDBReadOnlyAccess": "arn:{}:iam::aws:policy/AmazonDynamoDBReadOnlyAccess".format(partition), "AWSLambdaRole": "arn:{}:iam::aws:policy/service-role/AWSLambdaRole".format(partition), } + if partition == "aws": + mock_policy_loader.load.return_value[ + "AWSXrayWriteOnlyAccess" + ] = "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess" + else: + mock_policy_loader.load.return_value[ + "AWSXRayDaemonWriteAccess" + ] = "arn:{}:iam::aws:policy/AWSXRayDaemonWriteAccess".format(partition) output_fragment = transform(manifest, parameter_values, mock_policy_loader) @@ -621,6 +631,7 @@ def _generate_new_deployment_hash(self, logical_id, dict_to_hash, rest_api_to_sw "error_function_no_codeuri", "error_function_no_handler", "error_function_no_runtime", + "error_function_with_deployment_preference_invalid_alarms", "error_function_with_deployment_preference_missing_alias", "error_function_with_invalid_deployment_preference_hook_property", "error_function_invalid_request_parameters", @@ -673,6 +684,7 @@ def _generate_new_deployment_hash(self, logical_id, dict_to_hash, rest_api_to_sw "error_http_api_invalid_openapi", "error_http_api_tags", "error_http_api_tags_def_uri", + "error_implicit_http_api_properties", "error_implicit_http_api_method", "error_implicit_http_api_path", "error_http_api_event_multiple_same_path", diff --git a/versions/2016-10-31.md b/versions/2016-10-31.md index baea8f300a..fd020a5db1 100644 --- a/versions/2016-10-31.md +++ b/versions/2016-10-31.md @@ -430,6 +430,7 @@ Properties: - [DynamoDB](#dynamodb) - [SQS](#sqs) - [Api](#api) + - [HttpApi](#httpapi) - [Schedule](#schedule) - [CloudWatchEvent](#cloudwatchevent) - [EventBridgeRule](#eventbridgerule)