From c00750137b49eec8921d6aa7675962cdd37f25fb Mon Sep 17 00:00:00 2001 From: aws-sam-cli-bot <46753707+aws-sam-cli-bot@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:31:52 -0800 Subject: [PATCH 01/11] chore: Bump SamTranslator version to 1.58.1 (#4613) --- requirements/base.txt | 2 +- requirements/reproducible-linux.txt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 2166622726..fc7f54fb06 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -9,7 +9,7 @@ jmespath~=0.10.0 ruamel_yaml==0.17.21 PyYAML>=5.4.1,==5.* cookiecutter~=2.1.1 -aws-sam-translator==1.58.0 +aws-sam-translator==1.58.1 #docker minor version updates can include breaking changes. Auto update micro version only. docker~=4.2.0 dateparser~=1.0 diff --git a/requirements/reproducible-linux.txt b/requirements/reproducible-linux.txt index ef346d36be..92dca74a04 100644 --- a/requirements/reproducible-linux.txt +++ b/requirements/reproducible-linux.txt @@ -19,10 +19,10 @@ aws-lambda-builders==1.24.0 \ --hash=sha256:6e46ce9365edb20259acae4a21f41fa46c701aaa58d2fb681022e4f0998de2d1 \ --hash=sha256:fd7277e01a3c280c5a2a5ca5eb7888594ecddbc8355d1f519ea48a1f07f9d2d8 # via aws-sam-cli (setup.py) -aws-sam-translator==1.58.0 \ - --hash=sha256:627997303bcfb69209bc752f6b5b28b665b07341cec353d3711b05fc30e21ef8 \ - --hash=sha256:74eff244a4923320e5df2f37617d85505356353e6022ae9812c6f0abcfbad5d3 \ - --hash=sha256:9aaa3070a205669fdb3821b0c3eccaba1ff7917327c0e7d23dcc16d131d5dc30 +aws-sam-translator==1.58.1 \ + --hash=sha256:c4e261e450d574572d389edcafab04d1fe337615f867610410390c2435cb1f26 \ + --hash=sha256:ca47d6eb04d8cf358bea9160411193da40a80dc3e79bb0c5bace0c21f0e4c888 \ + --hash=sha256:cd60a19085d432bc00769b597bc2e6854f546ff9928f8067fc5fbcb5a1ed74ff # via # aws-sam-cli (setup.py) # cfn-lint From 6585b46bff1e7c123965b24795fb4e272be5d399 Mon Sep 17 00:00:00 2001 From: aws-sam-cli-bot <46753707+aws-sam-cli-bot@users.noreply.github.com> Date: Wed, 25 Jan 2023 11:35:09 -0800 Subject: [PATCH 02/11] chore: Bump AWS Lambda Builders version to 1.25.0 (#4617) Co-authored-by: Daniel Mil <84205762+mildaniel@users.noreply.github.com> --- requirements/base.txt | 2 +- requirements/reproducible-linux.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index fc7f54fb06..dbbd8cd4ce 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -15,7 +15,7 @@ docker~=4.2.0 dateparser~=1.0 requests==2.25.1 serverlessrepo==0.1.10 -aws_lambda_builders==1.24.0 +aws_lambda_builders==1.25.0 tomlkit==0.7.2 watchdog==2.1.2 pyopenssl==23.0.0 diff --git a/requirements/reproducible-linux.txt b/requirements/reproducible-linux.txt index 92dca74a04..08bc47104a 100644 --- a/requirements/reproducible-linux.txt +++ b/requirements/reproducible-linux.txt @@ -15,9 +15,9 @@ attrs==20.3.0 \ # jschema-to-python # jsonschema # sarif-om -aws-lambda-builders==1.24.0 \ - --hash=sha256:6e46ce9365edb20259acae4a21f41fa46c701aaa58d2fb681022e4f0998de2d1 \ - --hash=sha256:fd7277e01a3c280c5a2a5ca5eb7888594ecddbc8355d1f519ea48a1f07f9d2d8 +aws-lambda-builders==1.25.0 \ + --hash=sha256:4bb736a74457f87883861d57c0f6a859bd4e047b78ee58e09d16703a0c5172f3 \ + --hash=sha256:f9d2094f714434b3668377fee5729c883849aede8a64eafe689fff08a530783b # via aws-sam-cli (setup.py) aws-sam-translator==1.58.1 \ --hash=sha256:c4e261e450d574572d389edcafab04d1fe337615f867610410390c2435cb1f26 \ From c6dbb48bdc3245f90557c556ded2bca8fa04ff4b Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Wed, 25 Jan 2023 17:17:27 -0800 Subject: [PATCH 03/11] feat: List Command (#4587) * feat: Add table view and rich table (#3851) * Add table view and rich table * Black reformat * Update reproducible reqs * Comment out table comparison assertions * Make rich table class members protected * feat: Adding base commands and help messages for sam list (#3912) * Added the base commands of sam list and their corresponding help messages * Added no-args handling to base commands and added files for integration test suite * Made additions to resources integration tests * Made additions to the sam list integration test suite * Added to sam list integration and unit tests * Added integration tests to test help messages of sam list commands * Reformatted files * Cleaned up unfinished tests * adding check to see what the appveyor test will produce. Trying to resolve test failure * Fixed test to check help messages * modified pattern matching for eliminating newlines when matching help message * Changed the way whitespaces are handled in matching help messages * Addressed PR comments, moved items into common classes * Made modifications based on comments, removed relative import paths, added method annotations, fixed text formatting * Reformatted files * removed folder deletion * removed uneccessary folder creation and deletion * fixed errors with cwd of integration tests Co-authored-by: Andrew Zhan * Added the stack-outputs command implementation and tests (#3947) Co-authored-by: Andrew Zhan * Refactored stack outputs command to the producer mapper consumer design pattern (#3980) * Added the base commands of sam list and their corresponding help messages * Added no-args handling to base commands and added files for integration test suite * Made additions to resources integration tests * Made additions to the sam list integration test suite * Added to sam list integration and unit tests * Added integration tests to test help messages of sam list commands * Reformatted files * Cleaned up unfinished tests * adding check to see what the appveyor test will produce. Trying to resolve test failure * Fixed test to check help messages * modified pattern matching for eliminating newlines when matching help message * Changed the way whitespaces are handled in matching help messages * Addressed PR comments, moved items into common classes * Made modifications based on comments, removed relative import paths, added method annotations, fixed text formatting * Reformatted files * removed folder deletion * removed uneccessary folder creation and deletion * fixed errors with cwd of integration tests * Added implementation and tests for the stack-outputs command * Added test skips for integration tests, added unit tests, removed redundant init_client call * commit to retrigger appveyor tests * Commmit to trigger appveyor * Modified client source, made fixes based on comments * Made fixes based on comments * Combined get_stack_info and stack_exists, and modified unit tests * Empty-Commit * Empty-Commit * Empty-Commit * fixed tests based on comments * reformatted file * Refactored stack outputs command to the producer mapper consumer design pattern * Fixed formatting * Moved interfaces, made changes based on comments * Made fixes based on comments * Made fixes based on comments * Empty commit * Made changes based on comments, added new exceptions * Fixed format * Fixed return type declaration * Fixed return type declaration * Changed return type to list * Fixed error Co-authored-by: Andrew Zhan * Local transform and resource collection (#4020) Co-authored-by: Andrew Zhan * feat: Adding cloud resources to sam list resources output (#4056) * Added the base commands of sam list and their corresponding help messages * Added no-args handling to base commands and added files for integration test suite * Made additions to resources integration tests * Made additions to the sam list integration test suite * Added to sam list integration and unit tests * Added integration tests to test help messages of sam list commands * Reformatted files * Cleaned up unfinished tests * adding check to see what the appveyor test will produce. Trying to resolve test failure * Fixed test to check help messages * modified pattern matching for eliminating newlines when matching help message * Changed the way whitespaces are handled in matching help messages * Addressed PR comments, moved items into common classes * Made modifications based on comments, removed relative import paths, added method annotations, fixed text formatting * Reformatted files * removed folder deletion * removed uneccessary folder creation and deletion * fixed errors with cwd of integration tests * Added implementation and tests for the stack-outputs command * Added test skips for integration tests, added unit tests, removed redundant init_client call * commit to retrigger appveyor tests * Commmit to trigger appveyor * Modified client source, made fixes based on comments * Made fixes based on comments * Combined get_stack_info and stack_exists, and modified unit tests * Empty-Commit * Empty-Commit * Empty-Commit * fixed tests based on comments * reformatted file * Refactored stack outputs command to the producer mapper consumer design pattern * Fixed formatting * Moved interfaces, made changes based on comments * Made fixes based on comments * Made fixes based on comments * Empty commit * Made changes based on comments, added new exceptions * Fixed format * Fixed return type declaration * Fixed return type declaration * Changed return type to list * Fixed error * Implementation of the local transform and resource collection * Empty-Commit * Added section to avoid unused variable * Refactored common code * Added tests, modified PR * Fixed formatting * Made fixes based on PR comments * Fixed formatting * Fixed typing errors * Reverted typing * Fixed error with typing * Made changes to handling optional params * Fixes to typing errors * Made edits based on comments * Fixed error * Changed return type * Reverted return type due to make pr error * Added change to fix make pr error * Removed translate_utils.py file * Added cloud resources to sam list resources output * Empty commit * modified test format * Modified tests * Modified test Co-authored-by: Andrew Zhan * feat: Adding the sam list testable resources command, tests, and table output format support for all sam list commands (#4081) * Added the base commands of sam list and their corresponding help messages * Added no-args handling to base commands and added files for integration test suite * Made additions to resources integration tests * Made additions to the sam list integration test suite * Added to sam list integration and unit tests * Added integration tests to test help messages of sam list commands * Reformatted files * Cleaned up unfinished tests * adding check to see what the appveyor test will produce. Trying to resolve test failure * Fixed test to check help messages * modified pattern matching for eliminating newlines when matching help message * Changed the way whitespaces are handled in matching help messages * Addressed PR comments, moved items into common classes * Made modifications based on comments, removed relative import paths, added method annotations, fixed text formatting * Reformatted files * removed folder deletion * removed uneccessary folder creation and deletion * fixed errors with cwd of integration tests * Added implementation and tests for the stack-outputs command * Added test skips for integration tests, added unit tests, removed redundant init_client call * commit to retrigger appveyor tests * Commmit to trigger appveyor * Modified client source, made fixes based on comments * Made fixes based on comments * Combined get_stack_info and stack_exists, and modified unit tests * Empty-Commit * Empty-Commit * Empty-Commit * fixed tests based on comments * reformatted file * Refactored stack outputs command to the producer mapper consumer design pattern * Fixed formatting * Moved interfaces, made changes based on comments * Made fixes based on comments * Made fixes based on comments * Empty commit * Made changes based on comments, added new exceptions * Fixed format * Fixed return type declaration * Fixed return type declaration * Changed return type to list * Fixed error * Implementation of the local transform and resource collection * Empty-Commit * Added section to avoid unused variable * Refactored common code * Added tests, modified PR * Fixed formatting * Made fixes based on PR comments * Fixed formatting * Fixed typing errors * Reverted typing * Fixed error with typing * Made changes to handling optional params * Fixes to typing errors * Made edits based on comments * Fixed error * Changed return type * Reverted return type due to make pr error * Added change to fix make pr error * Removed translate_utils.py file * Added cloud resources to sam list resources output * Empty commit * modified test format * Modified tests * Modified test * Adding the sam list testable resources command, tests, and table output format support for all sam list commands * Changed table and made changes based on pr comments * Fixed integration test expected outputs * Fixed table heading * Added docstring and re-arranged the testable resources producer to reduce if-elses within a single function * Fixed format Co-authored-by: Andrew Zhan * Renaming the 'testable-resources' command to 'endpoints' (#4116) * Added the base commands of sam list and their corresponding help messages * Added no-args handling to base commands and added files for integration test suite * Made additions to resources integration tests * Made additions to the sam list integration test suite * Added to sam list integration and unit tests * Added integration tests to test help messages of sam list commands * Reformatted files * Cleaned up unfinished tests * adding check to see what the appveyor test will produce. Trying to resolve test failure * Fixed test to check help messages * modified pattern matching for eliminating newlines when matching help message * Changed the way whitespaces are handled in matching help messages * Addressed PR comments, moved items into common classes * Made modifications based on comments, removed relative import paths, added method annotations, fixed text formatting * Reformatted files * removed folder deletion * removed uneccessary folder creation and deletion * fixed errors with cwd of integration tests * Added implementation and tests for the stack-outputs command * Added test skips for integration tests, added unit tests, removed redundant init_client call * commit to retrigger appveyor tests * Commmit to trigger appveyor * Modified client source, made fixes based on comments * Made fixes based on comments * Combined get_stack_info and stack_exists, and modified unit tests * Empty-Commit * Empty-Commit * Empty-Commit * fixed tests based on comments * reformatted file * Refactored stack outputs command to the producer mapper consumer design pattern * Fixed formatting * Moved interfaces, made changes based on comments * Made fixes based on comments * Made fixes based on comments * Empty commit * Made changes based on comments, added new exceptions * Fixed format * Fixed return type declaration * Fixed return type declaration * Changed return type to list * Fixed error * Implementation of the local transform and resource collection * Empty-Commit * Added section to avoid unused variable * Refactored common code * Added tests, modified PR * Fixed formatting * Made fixes based on PR comments * Fixed formatting * Fixed typing errors * Reverted typing * Fixed error with typing * Made changes to handling optional params * Fixes to typing errors * Made edits based on comments * Fixed error * Changed return type * Reverted return type due to make pr error * Added change to fix make pr error * Removed translate_utils.py file * Added cloud resources to sam list resources output * Empty commit * modified test format * Modified tests * Modified test * Adding the sam list testable resources command, tests, and table output format support for all sam list commands * Changed table and made changes based on pr comments * Fixed integration test expected outputs * Fixed table heading * Added docstring and re-arranged the testable resources producer to reduce if-elses within a single function * Fixed format * Renamed command from testable resources to endpoints Co-authored-by: Andrew Zhan * Cleanup integration tests * Cleanup tests, address comments * Move boto3 imports, update unit tests * Add comments, use constants for resources * Update unit test mocking type * Add missing parameters * Add additional comments * Fix spelling, minor updates Co-authored-by: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Co-authored-by: andrewzhan Co-authored-by: Andrew Zhan --- samcli/cli/command.py | 2 + samcli/commands/list/__init__.py | 0 samcli/commands/list/cli_common/__init__.py | 0 .../list/cli_common/list_common_context.py | 26 + samcli/commands/list/cli_common/options.py | 34 + samcli/commands/list/endpoints/__init__.py | 0 samcli/commands/list/endpoints/command.py | 52 + .../list/endpoints/endpoints_context.py | 84 ++ samcli/commands/list/exceptions.py | 57 + samcli/commands/list/json_consumer.py | 14 + samcli/commands/list/list.py | 22 + samcli/commands/list/resources/__init__.py | 0 samcli/commands/list/resources/command.py | 52 + .../list/resources/resources_context.py | 57 + .../commands/list/stack_outputs/__init__.py | 0 samcli/commands/list/stack_outputs/command.py | 52 + .../stack_outputs/stack_outputs_context.py | 45 + samcli/commands/list/table_consumer.py | 42 + samcli/commands/validate/validate.py | 7 +- samcli/lib/list/__init__.py | 0 samcli/lib/list/data_to_json_mapper.py | 12 + samcli/lib/list/endpoints/__init__.py | 0 samcli/lib/list/endpoints/endpoints_def.py | 17 + .../lib/list/endpoints/endpoints_producer.py | 494 +++++++++ .../endpoints/endpoints_to_table_mapper.py | 83 ++ samcli/lib/list/list_interfaces.py | 87 ++ samcli/lib/list/mapper_consumer_container.py | 11 + samcli/lib/list/mapper_consumer_factory.py | 51 + samcli/lib/list/resources/__init__.py | 0 .../resources/resource_mapping_producer.py | 151 +++ samcli/lib/list/resources/resources_def.py | 10 + .../resources/resources_to_table_mapper.py | 48 + samcli/lib/list/stack_outputs/__init__.py | 0 .../stack_output_to_table_mapper.py | 46 + .../lib/list/stack_outputs/stack_outputs.py | 11 + .../stack_outputs/stack_outputs_producer.py | 70 ++ samcli/lib/providers/exceptions.py | 1 - samcli/lib/providers/sam_stack_provider.py | 21 +- samcli/lib/translate/__init__.py | 0 .../translate}/sam_template_validator.py | 5 +- samcli/lib/utils/resources.py | 6 + .../lib/test_sam_template_validator.py | 18 +- tests/integration/list/__init__.py | 0 tests/integration/list/endpoints/__init__.py | 0 .../list/endpoints/endpoints_integ_base.py | 27 + .../list/endpoints/test_endpoints_command.py | 133 +++ tests/integration/list/list_integ_base.py | 36 + tests/integration/list/resources/__init__.py | 0 .../list/resources/resources_integ_base.py | 27 + .../list/resources/test_resources_command.py | 123 +++ .../list/stack_outputs/__init__.py | 0 .../stack_outputs/stack_outputs_integ_base.py | 42 + .../test_stack_outputs_command.py | 109 ++ .../testdata/list/hello_world/__init__.py | 0 .../testdata/list/hello_world/app.py | 42 + .../list/hello_world/requirements.txt | 1 + .../list/test_endpoints_template.yaml | 44 + .../test_resources_invalid_sam_template.yaml | 42 + .../list/test_stack_creation_template.yaml | 39 + .../list/test_stack_no_outputs_template.yaml | 26 + tests/unit/commands/list/__init__.py | 0 .../unit/commands/list/endpoints/__init__.py | 0 .../unit/commands/list/endpoints/test_cli.py | 36 + .../list/endpoints/test_endpoints_context.py | 987 ++++++++++++++++++ .../unit/commands/list/resources/__init__.py | 0 .../unit/commands/list/resources/test_cli.py | 36 + .../list/resources/test_resources_context.py | 591 +++++++++++ .../commands/list/stack_outputs/__init__.py | 0 .../commands/list/stack_outputs/test_cli.py | 33 + .../test_stack_outputs_context.py | 101 ++ tests/unit/commands/list/test_list_mappers.py | 105 ++ .../lib/test_sam_template_validator.py | 18 +- tests/unit/commands/validate/test_cli.py | 24 +- .../iac/cfn/test_cfn_iac_implementation.py | 3 +- 74 files changed, 4271 insertions(+), 42 deletions(-) create mode 100644 samcli/commands/list/__init__.py create mode 100644 samcli/commands/list/cli_common/__init__.py create mode 100644 samcli/commands/list/cli_common/list_common_context.py create mode 100644 samcli/commands/list/cli_common/options.py create mode 100644 samcli/commands/list/endpoints/__init__.py create mode 100644 samcli/commands/list/endpoints/command.py create mode 100644 samcli/commands/list/endpoints/endpoints_context.py create mode 100644 samcli/commands/list/exceptions.py create mode 100644 samcli/commands/list/json_consumer.py create mode 100644 samcli/commands/list/list.py create mode 100644 samcli/commands/list/resources/__init__.py create mode 100644 samcli/commands/list/resources/command.py create mode 100644 samcli/commands/list/resources/resources_context.py create mode 100644 samcli/commands/list/stack_outputs/__init__.py create mode 100644 samcli/commands/list/stack_outputs/command.py create mode 100644 samcli/commands/list/stack_outputs/stack_outputs_context.py create mode 100644 samcli/commands/list/table_consumer.py create mode 100644 samcli/lib/list/__init__.py create mode 100644 samcli/lib/list/data_to_json_mapper.py create mode 100644 samcli/lib/list/endpoints/__init__.py create mode 100644 samcli/lib/list/endpoints/endpoints_def.py create mode 100644 samcli/lib/list/endpoints/endpoints_producer.py create mode 100644 samcli/lib/list/endpoints/endpoints_to_table_mapper.py create mode 100644 samcli/lib/list/list_interfaces.py create mode 100644 samcli/lib/list/mapper_consumer_container.py create mode 100644 samcli/lib/list/mapper_consumer_factory.py create mode 100644 samcli/lib/list/resources/__init__.py create mode 100644 samcli/lib/list/resources/resource_mapping_producer.py create mode 100644 samcli/lib/list/resources/resources_def.py create mode 100644 samcli/lib/list/resources/resources_to_table_mapper.py create mode 100644 samcli/lib/list/stack_outputs/__init__.py create mode 100644 samcli/lib/list/stack_outputs/stack_output_to_table_mapper.py create mode 100644 samcli/lib/list/stack_outputs/stack_outputs.py create mode 100644 samcli/lib/list/stack_outputs/stack_outputs_producer.py create mode 100644 samcli/lib/translate/__init__.py rename samcli/{commands/validate/lib => lib/translate}/sam_template_validator.py (97%) create mode 100644 tests/integration/list/__init__.py create mode 100644 tests/integration/list/endpoints/__init__.py create mode 100644 tests/integration/list/endpoints/endpoints_integ_base.py create mode 100644 tests/integration/list/endpoints/test_endpoints_command.py create mode 100644 tests/integration/list/list_integ_base.py create mode 100644 tests/integration/list/resources/__init__.py create mode 100644 tests/integration/list/resources/resources_integ_base.py create mode 100644 tests/integration/list/resources/test_resources_command.py create mode 100644 tests/integration/list/stack_outputs/__init__.py create mode 100644 tests/integration/list/stack_outputs/stack_outputs_integ_base.py create mode 100644 tests/integration/list/stack_outputs/test_stack_outputs_command.py create mode 100644 tests/integration/testdata/list/hello_world/__init__.py create mode 100644 tests/integration/testdata/list/hello_world/app.py create mode 100644 tests/integration/testdata/list/hello_world/requirements.txt create mode 100644 tests/integration/testdata/list/test_endpoints_template.yaml create mode 100644 tests/integration/testdata/list/test_resources_invalid_sam_template.yaml create mode 100644 tests/integration/testdata/list/test_stack_creation_template.yaml create mode 100644 tests/integration/testdata/list/test_stack_no_outputs_template.yaml create mode 100644 tests/unit/commands/list/__init__.py create mode 100644 tests/unit/commands/list/endpoints/__init__.py create mode 100644 tests/unit/commands/list/endpoints/test_cli.py create mode 100644 tests/unit/commands/list/endpoints/test_endpoints_context.py create mode 100644 tests/unit/commands/list/resources/__init__.py create mode 100644 tests/unit/commands/list/resources/test_cli.py create mode 100644 tests/unit/commands/list/resources/test_resources_context.py create mode 100644 tests/unit/commands/list/stack_outputs/__init__.py create mode 100644 tests/unit/commands/list/stack_outputs/test_cli.py create mode 100644 tests/unit/commands/list/stack_outputs/test_stack_outputs_context.py create mode 100644 tests/unit/commands/list/test_list_mappers.py diff --git a/samcli/cli/command.py b/samcli/cli/command.py index 9206dfbc4f..f7d27340ba 100644 --- a/samcli/cli/command.py +++ b/samcli/cli/command.py @@ -25,6 +25,7 @@ "samcli.commands.traces", "samcli.commands.sync", "samcli.commands.pipeline.pipeline", + "samcli.commands.list.list", # We intentionally do not expose the `bootstrap` command for now. We might open it up later # "samcli.commands.bootstrap", ] @@ -42,6 +43,7 @@ "traces": "Fetch AWS X-Ray traces", "sync": "Sync a project to AWS", "pipeline": "Manage the continuous delivery of the application", + "list": "Fetch the state of your serverless application", } diff --git a/samcli/commands/list/__init__.py b/samcli/commands/list/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/commands/list/cli_common/__init__.py b/samcli/commands/list/cli_common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/commands/list/cli_common/list_common_context.py b/samcli/commands/list/cli_common/list_common_context.py new file mode 100644 index 0000000000..455314ec19 --- /dev/null +++ b/samcli/commands/list/cli_common/list_common_context.py @@ -0,0 +1,26 @@ +""" +Common context class to inherit from for sam list sub-commands +""" +from samcli.lib.utils.boto_utils import get_boto_client_provider_with_config + + +class ListContext: + def __init__(self): + self.cloudformation_client = None + self.client_provider = None + self.region = None + self.profile = None + + def init_clients(self) -> None: + """ + Initialize the clients being used by sam list. + """ + from boto3 import Session + + if not self.region: + session = Session() + self.region = session.region_name + + client_provider = get_boto_client_provider_with_config(region=self.region, profile=self.profile) + self.client_provider = client_provider + self.cloudformation_client = client_provider("cloudformation") diff --git a/samcli/commands/list/cli_common/options.py b/samcli/commands/list/cli_common/options.py new file mode 100644 index 0000000000..b214ec9da7 --- /dev/null +++ b/samcli/commands/list/cli_common/options.py @@ -0,0 +1,34 @@ +""" +Common CLI options shared by various commands +""" + +import click + + +def stack_name_click_option(): + return click.option( + "--stack-name", + help=( + "Name of corresponding deployed stack.(Not including " + "a stack name will only show local resources defined " + "in the template.) " + ), + type=click.STRING, + ) + + +def stack_name_option(f): + return stack_name_click_option()(f) + + +def output_click_option(): + return click.option( + "--output", + default="table", + help="Output the results from the command in a given " "output format (json or table). ", + type=click.Choice(["json", "table"], case_sensitive=False), + ) + + +def output_option(f): + return output_click_option()(f) diff --git a/samcli/commands/list/endpoints/__init__.py b/samcli/commands/list/endpoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/commands/list/endpoints/command.py b/samcli/commands/list/endpoints/command.py new file mode 100644 index 0000000000..7a783c3789 --- /dev/null +++ b/samcli/commands/list/endpoints/command.py @@ -0,0 +1,52 @@ +""" +Sets up the cli for resources +""" + +import click + +from samcli.commands._utils.command_exception_handler import command_exception_handler +from samcli.commands.list.cli_common.options import stack_name_option, output_option +from samcli.cli.main import pass_context, common_options, aws_creds_options, print_cmdline_args +from samcli.lib.utils.version_checker import check_newer_version +from samcli.lib.telemetry.metric import track_command +from samcli.commands._utils.options import template_option_without_build +from samcli.cli.cli_config_file import configuration_option, TomlProvider + + +HELP_TEXT = """ +Get a summary of the cloud endpoints in the stack.\n +This command will show both the cloud and local endpoints that can +be used with sam local and sam sync. Currently the endpoint resources +are Lambda functions and API Gateway API resources. +""" + + +@click.command(name="endpoints", help=HELP_TEXT) +@configuration_option(provider=TomlProvider(section="parameters")) +@stack_name_option +@output_option +@template_option_without_build +@aws_creds_options +@common_options +@pass_context +@track_command +@check_newer_version +@print_cmdline_args +@command_exception_handler +def cli(self, stack_name, output, template_file, config_file, config_env): + """ + `sam list endpoints` command entry point + """ + do_cli(stack_name=stack_name, output=output, region=self.region, profile=self.profile, template_file=template_file) + + +def do_cli(stack_name, output, region, profile, template_file): + """ + Implementation of the ``cli`` method + """ + from samcli.commands.list.endpoints.endpoints_context import EndpointsContext + + with EndpointsContext( + stack_name=stack_name, output=output, region=region, profile=profile, template_file=template_file + ) as endpoints_context: + endpoints_context.run() diff --git a/samcli/commands/list/endpoints/endpoints_context.py b/samcli/commands/list/endpoints/endpoints_context.py new file mode 100644 index 0000000000..9a32dcd10d --- /dev/null +++ b/samcli/commands/list/endpoints/endpoints_context.py @@ -0,0 +1,84 @@ +""" +Display of the Endpoints of a SAM stack +""" +import logging +from typing import Optional + +from samcli.commands.list.cli_common.list_common_context import ListContext +from samcli.lib.list.endpoints.endpoints_producer import EndpointsProducer +from samcli.lib.list.mapper_consumer_factory import MapperConsumerFactory +from samcli.lib.list.list_interfaces import ProducersEnum + +LOG = logging.getLogger(__name__) + + +class EndpointsContext(ListContext): + """ + Context class for sam list endpoints + """ + + def __init__( + self, stack_name: str, output: str, region: Optional[str], profile: Optional[str], template_file: Optional[str] + ): + """ + Parameters + ---------- + stack_name: str + The name of the stack + output: str + The format of the output, either json or table + region: Optional[str] + The region of the stack + profile: Optional[str] + Optional profile to be used + template_file: Optional[str] + The location of the template file. If one is not specified, the default will be "template.yaml" in the CWD + """ + super().__init__() + self.stack_name = stack_name + self.output = output + self.region = region + self.profile = profile + self.template_file = template_file + self.iam_client = None + self.cloudcontrol_client = None + self.apigateway_client = None + self.apigatewayv2_client = None + + def __enter__(self): + self.init_clients() + return self + + def __exit__(self, *args): + pass + + def init_clients(self) -> None: + """ + Initialize the clients being used by sam list. + """ + super().init_clients() + self.iam_client = self.client_provider("iam") + self.cloudcontrol_client = self.client_provider("cloudcontrol") + self.apigateway_client = self.client_provider("apigateway") + self.apigatewayv2_client = self.client_provider("apigatewayv2") + + def run(self) -> None: + """ + Get the resources for a stack + """ + factory = MapperConsumerFactory() + container = factory.create(producer=ProducersEnum.ENDPOINTS_PRODUCER, output=self.output) + endpoints_producer = EndpointsProducer( + stack_name=self.stack_name, + region=self.region, + profile=self.profile, + template_file=self.template_file, + cloudformation_client=self.cloudformation_client, + iam_client=self.iam_client, + cloudcontrol_client=self.cloudcontrol_client, + apigateway_client=self.apigateway_client, + apigatewayv2_client=self.apigatewayv2_client, + mapper=container.mapper, + consumer=container.consumer, + ) + endpoints_producer.produce() diff --git a/samcli/commands/list/exceptions.py b/samcli/commands/list/exceptions.py new file mode 100644 index 0000000000..9269249d0e --- /dev/null +++ b/samcli/commands/list/exceptions.py @@ -0,0 +1,57 @@ +""" +Exceptions for SAM list +""" + + +from samcli.commands.exceptions import UserException + + +class SamListError(UserException): + """ + Base exception for the 'sam list' command + """ + + def __init__(self, msg): + self.msg = msg + + message_fmt = "{msg}" + + super().__init__(message=message_fmt.format(msg=msg)) + + +class SamListUnknownClientError(SamListError): + """ + Used when boto3 API call raises an unexpected ClientError + """ + + +class SamListUnknownBotoCoreError(SamListError): + """ + Used when boto3 API call raises an unexpected BotoCoreError + """ + + +class SamListLocalResourcesNotFoundError(SamListError): + """ + Used when unable to retrieve local resources after performing a transform + """ + + +class NoOutputsForStackError(UserException): + def __init__(self, stack_name, region): + self.stack_name = stack_name + self.region = region + + message_fmt = f"Outputs do not exist for the input stack {stack_name} on Cloudformation in the region {region}" + + super().__init__(message=message_fmt.format(stack_name=self.stack_name, region=self.region)) + + +class StackDoesNotExistInRegionError(UserException): + def __init__(self, stack_name, region): + self.stack_name = stack_name + self.region = region + + message_fmt = f"The input stack {stack_name} does" f" not exist on Cloudformation in the region {region}" + + super().__init__(message=message_fmt.format(stack_name=self.stack_name, region=self.region)) diff --git a/samcli/commands/list/json_consumer.py b/samcli/commands/list/json_consumer.py new file mode 100644 index 0000000000..7fc3c61b5c --- /dev/null +++ b/samcli/commands/list/json_consumer.py @@ -0,0 +1,14 @@ +""" +The json consumer for 'sam list' +""" +import click +from samcli.lib.list.list_interfaces import ListInfoPullerConsumer + + +class StringConsumerJsonOutput(ListInfoPullerConsumer): + """ + Consumes string data and outputs it in json format + """ + + def consume(self, data: str) -> None: + click.echo(data) diff --git a/samcli/commands/list/list.py b/samcli/commands/list/list.py new file mode 100644 index 0000000000..31b15b46ad --- /dev/null +++ b/samcli/commands/list/list.py @@ -0,0 +1,22 @@ +""" +Command group for "list" suite for commands. +""" + +import click + +from samcli.commands.list.resources.command import cli as resources_cli +from samcli.commands.list.stack_outputs.command import cli as stack_outputs_cli +from samcli.commands.list.endpoints.command import cli as testable_resources_cli + + +@click.group() +def cli(): + """ + Get local and deployed state of serverless application. + """ + + +# Add individual commands under this group +cli.add_command(resources_cli) +cli.add_command(stack_outputs_cli) +cli.add_command(testable_resources_cli) diff --git a/samcli/commands/list/resources/__init__.py b/samcli/commands/list/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/commands/list/resources/command.py b/samcli/commands/list/resources/command.py new file mode 100644 index 0000000000..7e00460090 --- /dev/null +++ b/samcli/commands/list/resources/command.py @@ -0,0 +1,52 @@ +""" +Sets up the cli for resources +""" + +import click + +from samcli.commands._utils.command_exception_handler import command_exception_handler +from samcli.commands.list.cli_common.options import stack_name_option, output_option +from samcli.cli.main import pass_context, common_options, aws_creds_options, print_cmdline_args +from samcli.lib.utils.version_checker import check_newer_version +from samcli.lib.telemetry.metric import track_command +from samcli.commands._utils.options import template_option_without_build +from samcli.cli.cli_config_file import configuration_option, TomlProvider + + +HELP_TEXT = """ +Get a list of resources that will be deployed to CloudFormation.\n +If a stack name is provided, the corresponding physical IDs of each +resource will be mapped to the logical ID of each resource. +""" + + +@click.command(name="resources", help=HELP_TEXT) +@configuration_option(provider=TomlProvider(section="parameters")) +@stack_name_option +@output_option +@template_option_without_build +@aws_creds_options +@common_options +@pass_context +@track_command +@check_newer_version +@print_cmdline_args +@command_exception_handler +def cli(self, stack_name, output, template_file, config_file, config_env): + """ + `sam list resources` command entry point + """ + + do_cli(stack_name=stack_name, output=output, region=self.region, profile=self.profile, template_file=template_file) + + +def do_cli(stack_name, output, region, profile, template_file): + """ + Implementation of the ``cli`` method + """ + from samcli.commands.list.resources.resources_context import ResourcesContext + + with ResourcesContext( + stack_name=stack_name, output=output, region=region, profile=profile, template_file=template_file + ) as resources_context: + resources_context.run() diff --git a/samcli/commands/list/resources/resources_context.py b/samcli/commands/list/resources/resources_context.py new file mode 100644 index 0000000000..de61b8167c --- /dev/null +++ b/samcli/commands/list/resources/resources_context.py @@ -0,0 +1,57 @@ +""" +Display the Resources of a SAM stack +""" +import logging +from typing import Optional + +from samcli.commands.list.cli_common.list_common_context import ListContext +from samcli.lib.list.resources.resource_mapping_producer import ResourceMappingProducer +from samcli.lib.list.mapper_consumer_factory import MapperConsumerFactory +from samcli.lib.list.list_interfaces import ProducersEnum + +LOG = logging.getLogger(__name__) + + +class ResourcesContext(ListContext): + def __init__( + self, stack_name: str, output: str, region: Optional[str], profile: Optional[str], template_file: Optional[str] + ): + super().__init__() + self.stack_name = stack_name + self.output = output + self.region = region + self.profile = profile + self.template_file = template_file + self.iam_client = None + + def __enter__(self): + self.init_clients() + return self + + def __exit__(self, *args): + pass + + def init_clients(self) -> None: + """ + Initialize the clients being used by sam list. + """ + super().init_clients() + self.iam_client = self.client_provider("iam") + + def run(self) -> None: + """ + Get the resources for a stack + """ + factory = MapperConsumerFactory() + container = factory.create(producer=ProducersEnum.RESOURCES_PRODUCER, output=self.output) + resource_producer = ResourceMappingProducer( + stack_name=self.stack_name, + region=self.region, + profile=self.profile, + template_file=self.template_file, + cloudformation_client=self.cloudformation_client, + iam_client=self.iam_client, + mapper=container.mapper, + consumer=container.consumer, + ) + resource_producer.produce() diff --git a/samcli/commands/list/stack_outputs/__init__.py b/samcli/commands/list/stack_outputs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/commands/list/stack_outputs/command.py b/samcli/commands/list/stack_outputs/command.py new file mode 100644 index 0000000000..1684084e44 --- /dev/null +++ b/samcli/commands/list/stack_outputs/command.py @@ -0,0 +1,52 @@ +""" +Sets up the cli for stack-outputs +""" + +import click + +from samcli.cli.cli_config_file import configuration_option, TomlProvider +from samcli.commands._utils.command_exception_handler import command_exception_handler +from samcli.commands.list.cli_common.options import output_option +from samcli.cli.main import pass_context, common_options, aws_creds_options, print_cmdline_args +from samcli.lib.utils.version_checker import check_newer_version +from samcli.lib.telemetry.metric import track_command + + +HELP_TEXT = """ +Get the stack outputs as defined in the SAM/CloudFormation template. +""" + + +@click.command(name="stack-outputs", help=HELP_TEXT) +@click.option( + "--stack-name", + help="Name of corresponding deployed stack. ", + required=True, + type=click.STRING, +) +@configuration_option(provider=TomlProvider(section="parameters")) +@output_option +@aws_creds_options +@common_options +@pass_context +@track_command +@check_newer_version +@print_cmdline_args +@command_exception_handler +def cli(self, stack_name, output, config_file, config_env): + """ + `sam list stack-outputs` command entry point + """ + do_cli(stack_name=stack_name, output=output, region=self.region, profile=self.profile) + + +def do_cli(stack_name, output, region, profile): + """ + Implementation of the ``cli`` method + """ + from samcli.commands.list.stack_outputs.stack_outputs_context import StackOutputsContext + + with StackOutputsContext( + stack_name=stack_name, output=output, region=region, profile=profile + ) as stack_output_context: + stack_output_context.run() diff --git a/samcli/commands/list/stack_outputs/stack_outputs_context.py b/samcli/commands/list/stack_outputs/stack_outputs_context.py new file mode 100644 index 0000000000..927b8731a8 --- /dev/null +++ b/samcli/commands/list/stack_outputs/stack_outputs_context.py @@ -0,0 +1,45 @@ +""" +Display the Outputs of a SAM stack +""" +import logging +from typing import Optional +from samcli.lib.list.stack_outputs.stack_outputs_producer import StackOutputsProducer +from samcli.commands.list.cli_common.list_common_context import ListContext +from samcli.lib.list.mapper_consumer_factory import MapperConsumerFactory +from samcli.lib.list.list_interfaces import ProducersEnum + +LOG = logging.getLogger(__name__) + + +class StackOutputsContext(ListContext): + def __init__(self, stack_name: str, output: str, region: Optional[str], profile: Optional[str]): + super().__init__() + self.stack_name = stack_name + self.output = output + self.region = region + self.profile = profile + self.cloudformation_client = None + + def __enter__(self): + self.init_clients() + return self + + def __exit__(self, *args): + pass + + def run(self) -> None: + """ + Get the stack outputs for a stack + """ + factory = MapperConsumerFactory() + container = factory.create(producer=ProducersEnum.STACK_OUTPUTS_PRODUCER, output=self.output) + + producer = StackOutputsProducer( + stack_name=self.stack_name, + output=self.output, + region=self.region, + cloudformation_client=self.cloudformation_client, + mapper=container.mapper, + consumer=container.consumer, + ) + producer.produce() diff --git a/samcli/commands/list/table_consumer.py b/samcli/commands/list/table_consumer.py new file mode 100644 index 0000000000..cbbdd52f97 --- /dev/null +++ b/samcli/commands/list/table_consumer.py @@ -0,0 +1,42 @@ +""" +The table consumer for 'sam list' +""" +from typing import Dict, Any +from samcli.lib.list.list_interfaces import ListInfoPullerConsumer +from samcli.commands._utils.table_print import pprint_column_names, pprint_columns + + +class StringConsumerTableOutput(ListInfoPullerConsumer): + """ + Outputs data in table format + """ + + def consume(self, data: Dict[Any, Any]) -> None: + """ + Outputs the data in a table format + Parameters + ---------- + data: Dict[Any, Any] + The data to be outputted + """ + + @pprint_column_names( + format_string=data["format_string"], + format_kwargs=data["format_args"], + table_header=data["table_name"], + ) + def print_table_rows(**kwargs): + """ + Prints the rows of the table based on the data provided + """ + for entry in data["data"]: + pprint_columns( + columns=entry, + width=kwargs["width"], + margin=kwargs["margin"], + format_string=data["format_string"], + format_args=kwargs["format_args"], + columns_dict=data["format_args"].copy(), + ) + + print_table_rows() diff --git a/samcli/commands/validate/validate.py b/samcli/commands/validate/validate.py index 28964e8a2f..d7ef7d384f 100644 --- a/samcli/commands/validate/validate.py +++ b/samcli/commands/validate/validate.py @@ -2,7 +2,6 @@ CLI Command for Validating a SAM Template """ import os - import boto3 from botocore.exceptions import NoCredentialsError import click @@ -50,8 +49,8 @@ def do_cli(ctx, template, lint): from samcli.commands.exceptions import UserException from samcli.commands.local.cli_common.user_exceptions import InvalidSamTemplateException - from .lib.exceptions import InvalidSamDocumentException - from .lib.sam_template_validator import SamTemplateValidator + from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException + from samcli.lib.translate.sam_template_validator import SamTemplateValidator if lint: _lint(ctx, template) @@ -64,7 +63,7 @@ def do_cli(ctx, template, lint): ) try: - validator.is_valid() + validator.get_translated_template_if_valid() except InvalidSamDocumentException as e: click.secho("Template provided at '{}' was invalid SAM Template.".format(template), bg="red") raise InvalidSamTemplateException(str(e)) from e diff --git a/samcli/lib/list/__init__.py b/samcli/lib/list/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/lib/list/data_to_json_mapper.py b/samcli/lib/list/data_to_json_mapper.py new file mode 100644 index 0000000000..920c82f582 --- /dev/null +++ b/samcli/lib/list/data_to_json_mapper.py @@ -0,0 +1,12 @@ +""" +Implementation of the data to json mapper +""" +from typing import Dict +import json +from samcli.lib.list.list_interfaces import Mapper + + +class DataToJsonMapper(Mapper): + def map(self, data: Dict[str, str]) -> str: + output = json.dumps(data, indent=2) + return output diff --git a/samcli/lib/list/endpoints/__init__.py b/samcli/lib/list/endpoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/lib/list/endpoints/endpoints_def.py b/samcli/lib/list/endpoints/endpoints_def.py new file mode 100644 index 0000000000..a411a0f619 --- /dev/null +++ b/samcli/lib/list/endpoints/endpoints_def.py @@ -0,0 +1,17 @@ +""" +The container for Endpoints +""" +from typing import Any +from dataclasses import dataclass + + +@dataclass +class EndpointsDef: + """ + Dataclass for containing entries of endpoints data + """ + + LogicalResourceId: str + PhysicalResourceId: str + CloudEndpoint: Any + Methods: Any diff --git a/samcli/lib/list/endpoints/endpoints_producer.py b/samcli/lib/list/endpoints/endpoints_producer.py new file mode 100644 index 0000000000..a3bc17b359 --- /dev/null +++ b/samcli/lib/list/endpoints/endpoints_producer.py @@ -0,0 +1,494 @@ +""" +The producer for the 'sam list endpoints' command +""" +import dataclasses +import logging +from typing import Dict, List, Any +from enum import Enum +import json +from botocore.exceptions import ClientError, BotoCoreError +from samcli.commands.list.exceptions import ( + SamListUnknownBotoCoreError, + SamListLocalResourcesNotFoundError, + SamListUnknownClientError, +) +from samcli.lib.list.list_interfaces import Producer +from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider +from samcli.lib.providers.provider import Stack +from samcli.commands._utils.template import get_template_data +from samcli.lib.list.endpoints.endpoints_def import EndpointsDef +from samcli.lib.list.resources.resource_mapping_producer import ResourceMappingProducer +from samcli.lib.utils.boto_utils import get_client_error_code +from samcli.lib.utils.resources import ( + AWS_LAMBDA_FUNCTION, + AWS_APIGATEWAY_RESTAPI, + AWS_APIGATEWAY_V2_API, + AWS_LAMBDA_FUNCTION_URL, + AWS_APIGATEWAY_BASE_PATH_MAPPING, + AWS_APIGATEWAY_v2_BASE_PATH_MAPPING, + AWS_APIGATEWAY_V2_DOMAIN_NAME, + AWS_APIGATWAY_DOMAIN_NAME, +) + +ENDPOINT_RESOURCE_TYPES = {AWS_LAMBDA_FUNCTION, AWS_APIGATEWAY_RESTAPI, AWS_APIGATEWAY_V2_API} +RESOURCE_DESCRIPTION = "ResourceDescription" +PROPERTIES = "Properties" +FUNCTION_URL = "FunctionUrl" +STACK_RESOURCES = "StackResources" +RESOURCE_TYPE = "ResourceType" +PHYSICAL_RESOURCE_ID = "PhysicalResourceId" +LOGICAL_RESOURCE_ID = "LogicalResourceId" +REST_API_ID = "RestApiId" +API_ID = "ApiId" +DOMAIN_NAME = "DomainName" +BODY = "Body" +PATHS = "paths" + +LOG = logging.getLogger(__name__) + + +class APIGatewayEnum(Enum): + API_GATEWAY = 1 + API_GATEWAY_V2 = 2 + + +class EndpointsProducer(ResourceMappingProducer, Producer): + def __init__( + self, + stack_name, + region, + profile, + template_file, + cloudformation_client, + iam_client, + cloudcontrol_client, + apigateway_client, + apigatewayv2_client, + mapper, + consumer, + ): + """ + Parameters + ---------- + stack_name: str + The name of the stack + region: Optional[str] + The region of the stack + profile: Optional[str] + Optional profile to be used + template_file: Optional[str] + The location of the template file. If one is not specified, the default will be "template.yaml" in the CWD + cloudformation_client: CloudFormation + The CloudFormation client + iam_client: IAM + The IAM client + cloudcontrol_client: CloudControl + The CloudControl client + apigateway_client: APIGateway + The APIGateway client + apigatewayv2_client: APIGatewayV2 + The APIGatewayV2 client + mapper: Mapper + The mapper used to map data to the format needed for the consumer provided + consumer: ListInfoPullerConsumer + The consumer used to output the data + """ + super().__init__( + stack_name, region, profile, template_file, cloudformation_client, iam_client, mapper, consumer + ) + self.stack_name = stack_name + self.region = region + self.profile = profile + self.template_file = template_file + self.cloudformation_client = cloudformation_client + self.iam_client = iam_client + self.cloudcontrol_client = cloudcontrol_client + self.apigateway_client = apigateway_client + self.apigatewayv2_client = apigatewayv2_client + self.mapper = mapper + self.consumer = consumer + + def get_function_url(self, identifier: str) -> Any: + """ + Gets the function url of a Lambda Function + + Parameters + ---------- + identifier: str + The identifier or physical ID + + Returns + ------- + furl: str + The function url in the form of a string + """ + try: + response = self.cloudcontrol_client.get_resource(TypeName=AWS_LAMBDA_FUNCTION_URL, Identifier=identifier) + if not response.get(RESOURCE_DESCRIPTION, {}).get(PROPERTIES, {}): + return "-" + response_dict = json.loads(response.get(RESOURCE_DESCRIPTION, {}).get(PROPERTIES, {})) + furl = response_dict.get(FUNCTION_URL, "-") + return furl + except ClientError as e: + if get_client_error_code(e) == "ResourceNotFoundException": + return "-" + LOG.error("ClientError Exception : %s", str(e)) + raise SamListUnknownClientError(msg=str(e)) from e + + def get_stage_list(self, api_id: str, api_type: APIGatewayEnum) -> List[Any]: + """ + Gets a list of stages for a given api of type AWS::ApiGateway::RestApi or AWS::ApiGatewayV2::Api + + Parameters + ---------- + api_id: str + The api id or rest api id of the api + api_type: APIGatewayEnum + The type of api, AWS::ApiGateway::RestApi or AWS::ApiGatewayV2::Api + + Returns + ------- + response_list: List[Any] + A list of stages for the api + """ + response_list: List[Any] + try: + response_list = [] + response: dict + search_key: str + stage_name_key: str + if api_type == APIGatewayEnum.API_GATEWAY: + response = self.apigateway_client.get_stages(restApiId=api_id) + search_key = "item" + stage_name_key = "stageName" + elif api_type == APIGatewayEnum.API_GATEWAY_V2: + response = self.apigatewayv2_client.get_stages(ApiId=api_id) + search_key = "Items" + stage_name_key = "StageName" + if not response.get(search_key, []): + return response_list + for item in response.get(search_key, []): + if item.get(stage_name_key, None): + response_list.append(item.get(stage_name_key, "")) + return response_list + except ClientError as e: + if get_client_error_code(e) == "NotFoundException": + return [] + LOG.error("ClientError Exception : %s", str(e)) + raise SamListUnknownClientError(msg=str(e)) from e + except BotoCoreError as e: + LOG.error("Botocore Exception : %s", str(e)) + raise SamListUnknownBotoCoreError(msg=str(e)) from e + + def build_api_gw_endpoints(self, physical_id: str, stages: list) -> list: + """ + Builds the default api gateway endpoints + + Parameters + ---------- + physical_id: str + The physical ID of the api resource + stages: list + A list of stages for the api resource + + Returns + ------- + api_list: List[Any] + The list of default api gateway endpoints + """ + api_list = [] + for stage in stages: + + api_list.append(f"https://{physical_id}.execute-api.{self.region}.amazonaws.com/{stage}") + return api_list + + def get_api_gateway_endpoint( + self, deployed_resource: Dict[Any, Any], custom_domain_substitute_dict: Dict[Any, Any] + ) -> Any: + """ + Gets the API gateway endpoints for APIGateway and APIGatewayV2 APIs + + Parameters + ---------- + deployed_resource: Dict[Any, Any] + Dictionary containing the resource info of the deployed API + custom_domain_substitute_dict: Dict[Any, Any] + Dictionary containing the mappings of the custom domains for APIs + + Returns + ------- + endpoint: Any + The endpoint(s) of the current API resource + """ + endpoint: Any + stages = self.get_stage_list( + deployed_resource.get(PHYSICAL_RESOURCE_ID, ""), + get_api_type_enum(deployed_resource.get(RESOURCE_TYPE, "")), + ) + if deployed_resource.get(LOGICAL_RESOURCE_ID, "") in custom_domain_substitute_dict: + endpoint = custom_domain_substitute_dict.get(deployed_resource.get(LOGICAL_RESOURCE_ID, ""), "-") + else: + endpoint = self.build_api_gw_endpoints(deployed_resource.get(PHYSICAL_RESOURCE_ID, ""), stages) + return endpoint + + def get_cloud_endpoints(self, stacks: list) -> list: + """ + Gets a list of cloud endpoints resources + + Parameters + ---------- + stacks: list + A list containing the local stack + + Returns + ------- + endpoints_list: List[Any] + A list of cloud endpoints resources + """ + endpoints_list = [] + local_stack = stacks[0] + local_stack_resources = local_stack.resources + seen_endpoints = set() + response = self.get_resources_info() + response_domain_dict = get_response_domain_dict(response) + custom_domain_substitute_dict = get_custom_domain_substitute_list(response, stacks, response_domain_dict) + + # Iterate over the deployed resources, collect relevant endpoint data for functions and APIGW resources + for deployed_resource in response.get(STACK_RESOURCES, {}): + if deployed_resource.get(RESOURCE_TYPE, "") in ENDPOINT_RESOURCE_TYPES: + endpoint_function_url: Any + paths_and_methods: Any + endpoint_function_url = "-" + paths_and_methods = "-" + + # Collect function URLs + if deployed_resource.get(RESOURCE_TYPE, "") == AWS_LAMBDA_FUNCTION: + endpoint_function_url = self.get_function_url(deployed_resource.get(PHYSICAL_RESOURCE_ID, "")) + + # Collect APIGW endpoints and methods + elif deployed_resource.get(RESOURCE_TYPE, "") in (AWS_APIGATEWAY_RESTAPI, AWS_APIGATEWAY_V2_API): + endpoint_function_url = self.get_api_gateway_endpoint( + deployed_resource, custom_domain_substitute_dict + ) + paths_and_methods = get_methods_and_paths( + deployed_resource.get(LOGICAL_RESOURCE_ID, ""), local_stack + ) + + endpoint_data = EndpointsDef( + LogicalResourceId=deployed_resource.get(LOGICAL_RESOURCE_ID, "-"), + PhysicalResourceId=deployed_resource.get(PHYSICAL_RESOURCE_ID, "-"), + CloudEndpoint=endpoint_function_url, + Methods=paths_and_methods, + ) + endpoints_list.append(dataclasses.asdict(endpoint_data)) + seen_endpoints.add(deployed_resource.get(LOGICAL_RESOURCE_ID, "")) + + # Loop over resources all stack resources and collect data for resources not yet deployed + for local_resource in local_stack_resources: + local_resource_type = local_stack_resources.get(local_resource, {}).get("Type", "") + paths_and_methods = "-" + # Check if a resources has already been added to the endpoints list, if not, add it + if local_resource_type in ENDPOINT_RESOURCE_TYPES and local_resource not in seen_endpoints: + # We don't support function URLs locally, so this can only be APIGW endpoint data + if local_resource_type in (AWS_APIGATEWAY_RESTAPI, AWS_APIGATEWAY_V2_API): + paths_and_methods = get_methods_and_paths(local_resource, local_stack) + endpoint_data = EndpointsDef( + LogicalResourceId=local_resource, + PhysicalResourceId="-", + CloudEndpoint="-", + Methods=paths_and_methods, + ) + endpoints_list.append(dataclasses.asdict(endpoint_data)) + + return endpoints_list + + def produce(self): + """ + The producer function for the endpoints resources command + """ + sam_template = get_template_data(self.template_file) + + translated_dict = self.get_translated_dict(template_file_dict=sam_template) + stacks, _ = SamLocalStackProvider.get_stacks(template_file="", template_dictionary=translated_dict) + validate_stack(stacks) + + endpoints_list: list + + if self.stack_name: + endpoints_list = self.get_cloud_endpoints(stacks) + else: + endpoints_list = get_local_endpoints(stacks) + mapped_output = self.mapper.map(endpoints_list) + self.consumer.consume(mapped_output) + + +def validate_stack(stacks: list): + """ + Checks if the stack non-empty and contains stack resources and raises exceptions accordingly + + Parameters + ---------- + stacks: list + A list containing the stack + """ + + if not stacks or not hasattr(stacks[0], "resources") or not stacks[0].resources: + raise SamListLocalResourcesNotFoundError(msg="No local resources found.") + + +def get_local_endpoints(stacks: list) -> list: + """ + Gets a list of local endpoints resources based on the local stack + + Parameters + ---------- + stacks: list + A list containing the stack + + Returns + ------- + endpoints_list: list + A list containing the endpoints resources and their information + """ + endpoints_list = [] + paths_and_methods: Any + local_stack = stacks[0] + local_stack_resources = local_stack.resources + for local_resource in local_stack_resources: + local_resource_type = local_stack_resources.get(local_resource, {}).get("Type", "") + if local_resource_type in ENDPOINT_RESOURCE_TYPES: + paths_and_methods = "-" + if local_resource_type in (AWS_APIGATEWAY_RESTAPI, AWS_APIGATEWAY_V2_API): + paths_and_methods = get_methods_and_paths(local_resource, local_stack) + # Set the PhysicalID to "-" if there is no corresponding PhysicalID + endpoint_data = EndpointsDef( + LogicalResourceId=local_resource, + PhysicalResourceId="-", + CloudEndpoint="-", + Methods=paths_and_methods, + ) + endpoints_list.append(dataclasses.asdict(endpoint_data)) + return endpoints_list + + +def get_api_type_enum(resource_type: str) -> APIGatewayEnum: + """ + Gets the APIGatewayEnum associated with the input resource type + + Parameters + ---------- + resource_type: str + The type of the resource + + Returns + ------- + The APIGatewayEnum associated with the input resource type + """ + if resource_type == AWS_APIGATEWAY_V2_API: + return APIGatewayEnum.API_GATEWAY_V2 + return APIGatewayEnum.API_GATEWAY + + +def get_custom_domain_substitute_list( + response: Dict[Any, Any], stacks: list, response_domain_dict: Dict[str, str] +) -> Dict[Any, Any]: + """ + Gets a dictionary containing the custom domain lists that map back to the original api + + Parameters + ---------- + response: Dict[Any, Any] + The response containing the cloud stack resources information + stacks: list + A list containing the local stack + response_domain_dict: Dict + A dictionary containing the custom domains + Returns + ------- + custom_domain_substitute_dict: Dict[Any, Any] + A dict containing the custom domain lists mapped to the original apis + """ + custom_domain_substitute_dict = {} + local_stack = stacks[0] + local_stack_resources = local_stack.resources + for resource in response.get(STACK_RESOURCES, {}): + # Collect custom domain data for APIGW V1 resources + if resource.get(RESOURCE_TYPE, "") == AWS_APIGATEWAY_BASE_PATH_MAPPING: + local_mapping = local_stack_resources.get(resource.get(LOGICAL_RESOURCE_ID, ""), {}).get(PROPERTIES, {}) + rest_api_id = local_mapping.get(REST_API_ID, "") + domain_id = local_mapping.get(DOMAIN_NAME, "") + if domain_id in response_domain_dict: + if rest_api_id not in custom_domain_substitute_dict: + custom_domain_substitute_dict[rest_api_id] = [response_domain_dict.get(domain_id, None)] + else: + custom_domain_substitute_dict[rest_api_id].append(response_domain_dict.get(domain_id, None)) + + # Collect custom domain data for APIGW V2 resources + elif resource.get(RESOURCE_TYPE, "") == AWS_APIGATEWAY_v2_BASE_PATH_MAPPING: + local_mapping = local_stack_resources.get(resource.get(LOGICAL_RESOURCE_ID, ""), {}).get(PROPERTIES, {}) + rest_api_id = local_mapping.get(API_ID, "") + domain_id = local_mapping.get(DOMAIN_NAME, "") + if domain_id in response_domain_dict: + if rest_api_id not in custom_domain_substitute_dict: + custom_domain_substitute_dict[rest_api_id] = [response_domain_dict.get(domain_id, None)] + else: + custom_domain_substitute_dict[rest_api_id].append(response_domain_dict.get(domain_id, None)) + return custom_domain_substitute_dict + + +def get_response_domain_dict(response: Dict[Any, Any]) -> Dict[str, str]: + """ + Gets a dictionary containing the custom domains + + Parameters + ---------- + response: Dict[Any, Any] + The response containing the cloud stack resources information + + Returns + ------- + response_domain_dict: Dict[str, str] + A dict containing the custom domains + """ + response_domain_dict = {} + for resource in response.get(STACK_RESOURCES, {}): + if ( + resource.get(RESOURCE_TYPE, "") == AWS_APIGATWAY_DOMAIN_NAME + or resource.get(RESOURCE_TYPE, "") == AWS_APIGATEWAY_V2_DOMAIN_NAME + ): + response_domain_dict[ + resource.get(LOGICAL_RESOURCE_ID, "") + ] = f'https://{resource.get(PHYSICAL_RESOURCE_ID, "")}' + return response_domain_dict + + +def get_methods_and_paths(logical_id: str, stack: Stack) -> list: + """ + Gets the methods and paths for apis based on the stack and the logical ID + + Parameters + ---------- + logical_id: str + The logical ID of the api + stack: Stack + The stack to retrieve the methods and paths from + + Returns + ------- + method_paths_list: list + A list containing the methods and paths of the api + """ + method_paths_list: List[Any] + method_paths_list = [] + if not stack.resources: + raise SamListLocalResourcesNotFoundError(msg="No local resources found.") + if not stack.resources.get(logical_id, {}).get(PROPERTIES, {}).get(BODY, {}).get(PATHS, {}): + return method_paths_list + paths_dict = stack.resources.get(logical_id, {}).get(PROPERTIES, {}).get(BODY, {}).get(PATHS, {}) + for path in paths_dict: + method_list = [] + for method in paths_dict.get(path, ""): + method_list.append(method) + path_item = path + f"{method_list}" + method_paths_list.append(path_item) + return method_paths_list diff --git a/samcli/lib/list/endpoints/endpoints_to_table_mapper.py b/samcli/lib/list/endpoints/endpoints_to_table_mapper.py new file mode 100644 index 0000000000..8ef6296ff1 --- /dev/null +++ b/samcli/lib/list/endpoints/endpoints_to_table_mapper.py @@ -0,0 +1,83 @@ +""" +Implementation of the endpoints to table mapper +""" +from typing import Dict, Any +from collections import OrderedDict +from samcli.lib.list.list_interfaces import Mapper + +NO_DATA = "-" +SPACING = "" +CLOUD_ENDPOINT = "CloudEndpoint" +METHODS = "Methods" + + +class EndpointsToTableMapper(Mapper): + """ + Mapper class for mapping endpoints data for table output + """ + + def map(self, data: list) -> Dict[Any, Any]: + """ + Maps data to the format needed for consumption by the table consumer + + Parameters + ---------- + data: list + List of dictionaries containing the entries of the endpoints data + + Returns + ------- + table_data: Dict[Any, Any] + Dictionary containing the information and data needed for the table consumer + to output the data in table format + """ + entry_list = [] + + # Parse through the data object and separate out each data point we want to display. + # If the data is none, default to using a "-" + for endpoint in data: + cloud_endpoint_furl_string = endpoint.get(CLOUD_ENDPOINT, NO_DATA) + methods_string = NO_DATA + cloud_endpoint_furl_multi_list = [] + + # Build row of cloud endpoint data + if isinstance(endpoint.get(CLOUD_ENDPOINT, NO_DATA), list) and endpoint.get(CLOUD_ENDPOINT, []): + cloud_endpoint_furl_string = endpoint.get(CLOUD_ENDPOINT, [NO_DATA])[0] + if len(endpoint.get(CLOUD_ENDPOINT, [])) > 1: + cloud_endpoint_furl_multi_list = endpoint.get(CLOUD_ENDPOINT, [SPACING, SPACING])[1:] + + # Build row of methods data + if isinstance(endpoint.get(METHODS, NO_DATA), list) and endpoint.get(METHODS, []): + methods_string = "; ".join(endpoint.get(METHODS, [])) + + # Generate the list of endpoint data to be displayed. Each row displays an element in list, + # where each element is a list of the columns. + entry_list.append( + [ + endpoint.get("LogicalResourceId", NO_DATA), + endpoint.get("PhysicalResourceId", NO_DATA), + cloud_endpoint_furl_string, + methods_string, + ] + ) + + # Add a spacing column with the next endpoint in the table in case there are multiple endpoints to display. + if cloud_endpoint_furl_multi_list: + for url in cloud_endpoint_furl_multi_list: + entry_list.append([SPACING, SPACING, url, SPACING]) + + # Build out the table with the data collected to represent the endpoints + table_data = { + "format_string": "{Resource ID:<{0}} {Physical ID:<{1}} {Cloud Endpoints:<{2}} {Methods:<{3}}", + "format_args": OrderedDict( + { + "Resource ID": "Resource ID", + "Physical ID": "Physical ID", + "Cloud Endpoints": "Cloud Endpoints", + "Methods": "Methods", + } + ), + "table_name": "Endpoints", + "data": entry_list, + } + return table_data diff --git a/samcli/lib/list/list_interfaces.py b/samcli/lib/list/list_interfaces.py new file mode 100644 index 0000000000..07123cb676 --- /dev/null +++ b/samcli/lib/list/list_interfaces.py @@ -0,0 +1,87 @@ +""" +Interface for MapperConsumerFactory, Producer, Mapper, ListInfoPullerConsumer +""" +from abc import ABC, abstractmethod +from typing import Generic, TypeVar +from enum import Enum + +InputType = TypeVar("InputType") +OutputType = TypeVar("OutputType") + + +class ListInfoPullerConsumer(ABC, Generic[InputType]): + """ + Interface definition to consume and display data + """ + + @abstractmethod + def consume(self, data: InputType): + """ + Parameters + ---------- + data: TypeVar + Data for the consumer to print + """ + + +class Mapper(ABC, Generic[InputType, OutputType]): + """ + Interface definition to map data to json or table + """ + + @abstractmethod + def map(self, data: InputType) -> OutputType: + """ + Parameters + ---------- + data: TypeVar + Data for the mapper to map + + Returns + ------- + Any + Mapped output given the data + """ + + +class Producer(ABC): + """ + Interface definition to produce data for the mappers and consumers + """ + + mapper: Mapper + consumer: ListInfoPullerConsumer + + @abstractmethod + def produce(self): + """ + Produces the data for the mappers and consumers + """ + + +class MapperConsumerFactoryInterface(ABC): + """ + Interface definition to create mapper-consumer factories + """ + + @abstractmethod + def create(self, producer, output): + """ + Parameters + ---------- + producer: str + A string indicating which producer is calling the function + output: str + A string indicating the output type + + Returns + ------- + MapperConsumerContainer + A container that contains a mapper and a consumer + """ + + +class ProducersEnum(Enum): + STACK_OUTPUTS_PRODUCER = 1 + RESOURCES_PRODUCER = 2 + ENDPOINTS_PRODUCER = 3 diff --git a/samcli/lib/list/mapper_consumer_container.py b/samcli/lib/list/mapper_consumer_container.py new file mode 100644 index 0000000000..6be090c9cd --- /dev/null +++ b/samcli/lib/list/mapper_consumer_container.py @@ -0,0 +1,11 @@ +""" +Container for a mapper and a consumer +""" +from dataclasses import dataclass +from samcli.lib.list.list_interfaces import ListInfoPullerConsumer, Mapper + + +@dataclass +class MapperConsumerContainer: + mapper: Mapper + consumer: ListInfoPullerConsumer diff --git a/samcli/lib/list/mapper_consumer_factory.py b/samcli/lib/list/mapper_consumer_factory.py new file mode 100644 index 0000000000..b9722771ca --- /dev/null +++ b/samcli/lib/list/mapper_consumer_factory.py @@ -0,0 +1,51 @@ +""" +The factory for returning the appropriate mapper and consumer +""" +from samcli.lib.list.list_interfaces import MapperConsumerFactoryInterface +from samcli.lib.list.data_to_json_mapper import DataToJsonMapper +from samcli.commands.list.json_consumer import StringConsumerJsonOutput +from samcli.commands.list.table_consumer import StringConsumerTableOutput +from samcli.lib.list.mapper_consumer_container import MapperConsumerContainer +from samcli.lib.list.stack_outputs.stack_output_to_table_mapper import StackOutputToTableMapper +from samcli.lib.list.resources.resources_to_table_mapper import ResourcesToTableMapper +from samcli.lib.list.endpoints.endpoints_to_table_mapper import EndpointsToTableMapper +from samcli.lib.list.list_interfaces import ProducersEnum, Mapper + + +class MapperConsumerFactory(MapperConsumerFactoryInterface): + """ + Factory class to create factory objects that map a given producer and output format to a mapper and a consumer + """ + + def create(self, producer: ProducersEnum, output: str) -> MapperConsumerContainer: + """ + Creates a MapperConsumerContainer that contains the resulting mapper and consumer given + the producer and output format + + Parameters + ---------- + producer: ProducersEnum + An enum representing the producers (stack-outputs, resources, or endpoints producer) + output: str + The output format, either json or table + + Returns + ------- + container: MapperConsumerContainer + A MapperConsumerContainer containing the resulting mapper and consumer to be used by the producer + """ + if output == "json": + data_to_json_mapper = DataToJsonMapper() + json_consumer = StringConsumerJsonOutput() + container = MapperConsumerContainer(data_to_json_mapper, json_consumer) + return container + table_mapper: Mapper + table_consumer = StringConsumerTableOutput() + if producer == ProducersEnum.STACK_OUTPUTS_PRODUCER: + table_mapper = StackOutputToTableMapper() + elif producer == ProducersEnum.RESOURCES_PRODUCER: + table_mapper = ResourcesToTableMapper() + elif producer == ProducersEnum.ENDPOINTS_PRODUCER: + table_mapper = EndpointsToTableMapper() + container = MapperConsumerContainer(table_mapper, table_consumer) + return container diff --git a/samcli/lib/list/resources/__init__.py b/samcli/lib/list/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/lib/list/resources/resource_mapping_producer.py b/samcli/lib/list/resources/resource_mapping_producer.py new file mode 100644 index 0000000000..89b96662a5 --- /dev/null +++ b/samcli/lib/list/resources/resource_mapping_producer.py @@ -0,0 +1,151 @@ +""" +The producer for the 'sam list resources' command +""" +from typing import Any, Dict +import dataclasses +import logging +import yaml + +from botocore.exceptions import ClientError, NoCredentialsError, BotoCoreError +from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader +from samtranslator.translator.arn_generator import NoRegionFound + +from samcli.commands.list.exceptions import ( + SamListLocalResourcesNotFoundError, + SamListUnknownClientError, + StackDoesNotExistInRegionError, + SamListUnknownBotoCoreError, +) + +from samcli.lib.list.list_interfaces import Producer +from samcli.lib.list.resources.resources_def import ResourcesDef +from samcli.lib.translate.sam_template_validator import SamTemplateValidator +from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider +from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException +from samcli.commands.local.cli_common.user_exceptions import InvalidSamTemplateException +from samcli.commands.exceptions import UserException +from samcli.commands._utils.template import get_template_data +from samcli.lib.utils.boto_utils import get_client_error_code + + +LOG = logging.getLogger(__name__) + +ROOT_STACK = 0 + + +class ResourceMappingProducer(Producer): + def __init__( + self, + stack_name, + region, + profile, + template_file, + cloudformation_client, + iam_client, + mapper, + consumer, + ): + self.stack_name = stack_name + self.region = region + self.profile = profile + self.template_file = template_file + self.cloudformation_client = cloudformation_client + self.iam_client = iam_client + self.mapper = mapper + self.consumer = consumer + + def get_resources_info(self): + """ + Returns the stack resources information for the stack and raises exceptions accordingly + + Returns + ------- + A dictionary containing information about the stack's resources + """ + + try: + response = self.cloudformation_client.describe_stack_resources(StackName=self.stack_name) + if "StackResources" not in response: + return {"StackResources": []} + return response + except ClientError as e: + if get_client_error_code(e) == "ValidationError": + LOG.debug("Stack with id %s does not exist", self.stack_name) + raise StackDoesNotExistInRegionError(stack_name=self.stack_name, region=self.region) from e + LOG.error("ClientError Exception : %s", str(e)) + raise SamListUnknownClientError(msg=str(e)) from e + except BotoCoreError as e: + LOG.error("Botocore Exception : %s", str(e)) + raise SamListUnknownBotoCoreError(msg=str(e)) from e + + def get_translated_dict(self, template_file_dict: Dict[Any, Any]) -> Dict[Any, Any]: + """ + Performs a sam translate on a template and returns the translated template in the form of a dictionary or + raises exceptions accordingly + + Parameters + ---------- + template_file_dict: Dict[Any, Any] + The template in dictionary format to be translated + + Returns + ------- + response: Dict[Any, Any] + The dictionary representing the translated template + """ + try: + # Note to check if IAM can be mocked to get around doing a translate without it + validator = SamTemplateValidator( + template_file_dict, ManagedPolicyLoader(self.iam_client), profile=self.profile, region=self.region + ) + translated_dict: dict + translated_dict = yaml.load(validator.get_translated_template_if_valid(), Loader=yaml.FullLoader) + return translated_dict + except InvalidSamDocumentException as e: + raise InvalidSamTemplateException(str(e)) from e + except NoRegionFound as no_region_found_e: + raise UserException( + "AWS Region was not found. Please configure your region through a profile or --region option", + wrapped_from=no_region_found_e.__class__.__name__, + ) from no_region_found_e + except NoCredentialsError as e: + raise UserException( + "AWS Credentials are required. Please configure your credentials.", wrapped_from=e.__class__.__name__ + ) from e + except ClientError as e: + LOG.error("ClientError Exception : %s", str(e)) + raise SamListUnknownClientError(msg=str(e)) from e + + def produce(self): + """ + Produces the resource data to be printed + """ + sam_template = get_template_data(self.template_file) + + translated_dict = self.get_translated_dict(template_file_dict=sam_template) + + stacks, _ = SamLocalStackProvider.get_stacks(template_file="", template_dictionary=translated_dict) + if not stacks or not stacks[ROOT_STACK].resources: + raise SamListLocalResourcesNotFoundError(msg="No local resources found.") + seen_resources = set() + resources_list = [] + if self.stack_name: + response = self.get_resources_info() + for deployed_resource in response["StackResources"]: + resource_data = ResourcesDef( + LogicalResourceId=deployed_resource["LogicalResourceId"], + PhysicalResourceId=deployed_resource["PhysicalResourceId"], + ) + resources_list.append(dataclasses.asdict(resource_data)) + seen_resources.add(deployed_resource["LogicalResourceId"]) + for local_resource in stacks[ROOT_STACK].resources: + if local_resource not in seen_resources: + resource_data = ResourcesDef(LogicalResourceId=local_resource, PhysicalResourceId="-") + resources_list.append(dataclasses.asdict(resource_data)) + else: + for local_resource in stacks[ROOT_STACK].resources: + # Set the PhysicalID to "-" if there is no corresponding PhysicalID + resource_data = ResourcesDef(LogicalResourceId=local_resource, PhysicalResourceId="-") + resources_list.append(dataclasses.asdict(resource_data)) + mapped_output = self.mapper.map(resources_list) + self.consumer.consume(mapped_output) diff --git a/samcli/lib/list/resources/resources_def.py b/samcli/lib/list/resources/resources_def.py new file mode 100644 index 0000000000..37bebecbba --- /dev/null +++ b/samcli/lib/list/resources/resources_def.py @@ -0,0 +1,10 @@ +""" +The container for Resources +""" +from dataclasses import dataclass + + +@dataclass +class ResourcesDef: + LogicalResourceId: str + PhysicalResourceId: str diff --git a/samcli/lib/list/resources/resources_to_table_mapper.py b/samcli/lib/list/resources/resources_to_table_mapper.py new file mode 100644 index 0000000000..4cc209809b --- /dev/null +++ b/samcli/lib/list/resources/resources_to_table_mapper.py @@ -0,0 +1,48 @@ +""" +Implementation of the resources to table mapper +""" +from typing import Dict, Any +from collections import OrderedDict +from samcli.lib.list.list_interfaces import Mapper + + +class ResourcesToTableMapper(Mapper): + """ + Mapper class for mapping resources data for table output + """ + + def map(self, data: list) -> Dict[Any, Any]: + """ + Maps data to the format needed for consumption by the table consumer + + Parameters + ---------- + data: list + List of dictionaries containing the entries of the resources data + + Returns + ------- + table_data: Dict[Any, Any] + Dictionary containing the information and data needed for the table + consumer to output the data in table format + """ + entry_list = [] + for resource in data: + entry_list.append( + [ + resource.get("LogicalResourceId", "-"), + resource.get("PhysicalResourceId", "-"), + ] + ) + table_data = { + "format_string": "{Logical ID:<{0}} {Physical ID:<{1}}", + "format_args": OrderedDict( + { + "Logical ID": "Logical ID", + "Physical ID": "Physical ID", + } + ), + "table_name": "Resources", + "data": entry_list, + } + return table_data diff --git a/samcli/lib/list/stack_outputs/__init__.py b/samcli/lib/list/stack_outputs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/lib/list/stack_outputs/stack_output_to_table_mapper.py b/samcli/lib/list/stack_outputs/stack_output_to_table_mapper.py new file mode 100644 index 0000000000..3e0cc846f6 --- /dev/null +++ b/samcli/lib/list/stack_outputs/stack_output_to_table_mapper.py @@ -0,0 +1,46 @@ +""" +Implementation of the stack output to table mapper +""" +from typing import Dict, Any +from collections import OrderedDict +from samcli.lib.list.list_interfaces import Mapper + + +class StackOutputToTableMapper(Mapper): + """ + Mapper class for mapping stack-outputs data for table output + """ + + def map(self, data: list) -> Dict[Any, Any]: + """ + Maps data to the format needed for consumption by the table consumer + + Parameters + ---------- + data: list + List of dictionaries containing the entries of the stack outputs data + + Returns + ------- + table_data: Dict[Any, Any] + Dictionary containing the information and data needed for the table consumer + to output the data in table format + """ + entry_list = [] + for stack_output in data: + entry_list.append( + [ + stack_output.get("OutputKey", "-"), + stack_output.get("OutputValue", "-"), + stack_output.get("Description", "-"), + ] + ) + table_data = { + "format_string": "{OutputKey:<{0}} {OutputValue:<{1}} {Description:<{2}}", + "format_args": OrderedDict( + {"OutputKey": "OutputKey", "OutputValue": "OutputValue", "Description": "Description"} + ), + "table_name": "Stack Outputs", + "data": entry_list, + } + return table_data diff --git a/samcli/lib/list/stack_outputs/stack_outputs.py b/samcli/lib/list/stack_outputs/stack_outputs.py new file mode 100644 index 0000000000..292da23b48 --- /dev/null +++ b/samcli/lib/list/stack_outputs/stack_outputs.py @@ -0,0 +1,11 @@ +""" +The container for stack outputs +""" +from dataclasses import dataclass + + +@dataclass +class StackOutputs: + OutputKey: str + OutputValue: str + Description: str diff --git a/samcli/lib/list/stack_outputs/stack_outputs_producer.py b/samcli/lib/list/stack_outputs/stack_outputs_producer.py new file mode 100644 index 0000000000..a97b35856e --- /dev/null +++ b/samcli/lib/list/stack_outputs/stack_outputs_producer.py @@ -0,0 +1,70 @@ +""" +The producer for the 'sam list stack-outputs' command +""" +from typing import Optional, Any +import dataclasses +import logging + +from botocore.exceptions import ClientError, BotoCoreError +from samcli.commands.list.exceptions import ( + SamListUnknownClientError, + SamListUnknownBotoCoreError, + NoOutputsForStackError, + StackDoesNotExistInRegionError, +) + +from samcli.lib.list.list_interfaces import Producer +from samcli.lib.list.stack_outputs.stack_outputs import StackOutputs +from samcli.lib.utils.boto_utils import get_client_error_code + +LOG = logging.getLogger(__name__) + + +class StackOutputsProducer(Producer): + def __init__(self, stack_name, output, region, cloudformation_client, mapper, consumer): + self.stack_name = stack_name + self.output = output + self.region = region + self.cloudformation_client = cloudformation_client + self.mapper = mapper + self.consumer = consumer + + def get_stack_info(self) -> Optional[Any]: + """ + Returns the stack output information for the stack and raises exceptions accordingly + + Returns + ------- + A dictionary containing the stack's information + """ + + try: + response = self.cloudformation_client.describe_stacks(StackName=self.stack_name) + if not response.get("Stacks", []): + raise StackDoesNotExistInRegionError(stack_name=self.stack_name, region=self.region) + if len(response.get("Stacks", [])) > 0 and "Outputs" not in response.get("Stacks", [])[0]: + raise NoOutputsForStackError(stack_name=self.stack_name, region=self.region) + return response["Stacks"][0]["Outputs"] + + except ClientError as e: + if get_client_error_code(e) == "ValidationError": + LOG.debug("Stack with id %s does not exist", self.stack_name) + raise StackDoesNotExistInRegionError(stack_name=self.stack_name, region=self.region) from e + LOG.error("ClientError Exception : %s", str(e)) + raise SamListUnknownClientError(msg=str(e)) from e + except BotoCoreError as e: + LOG.error("Botocore Exception : %s", str(e)) + raise SamListUnknownBotoCoreError(msg=str(e)) from e + + def produce(self): + response = self.get_stack_info() + output_list = [] + for stack_output in response: + stack_output_data = StackOutputs( + OutputKey=stack_output["OutputKey"], + OutputValue=stack_output["OutputValue"], + Description=stack_output["Description"], + ) + output_list.append(dataclasses.asdict(stack_output_data)) + mapped_output = self.mapper.map(output_list) + self.consumer.consume(data=mapped_output) diff --git a/samcli/lib/providers/exceptions.py b/samcli/lib/providers/exceptions.py index ff462b1d01..1370bf9af8 100644 --- a/samcli/lib/providers/exceptions.py +++ b/samcli/lib/providers/exceptions.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING - if TYPE_CHECKING: # pragma: no cover from samcli.lib.providers.provider import ResourceIdentifier diff --git a/samcli/lib/providers/sam_stack_provider.py b/samcli/lib/providers/sam_stack_provider.py index 8425296c34..c6adf8476a 100644 --- a/samcli/lib/providers/sam_stack_provider.py +++ b/samcli/lib/providers/sam_stack_provider.py @@ -11,6 +11,7 @@ from samcli.lib.providers.provider import Stack, get_full_path from samcli.lib.providers.sam_base_provider import SamBaseProvider from samcli.lib.utils.resources import AWS_CLOUDFORMATION_STACK, AWS_SERVERLESS_APPLICATION +from samcli.commands._utils.template import TemplateNotFoundException LOG = logging.getLogger(__name__) @@ -192,12 +193,13 @@ def _convert_cfn_stack_resource( @staticmethod def get_stacks( - template_file: str, + template_file: Optional[str] = None, stack_path: str = "", name: str = "", parameter_overrides: Optional[Dict] = None, global_parameter_overrides: Optional[Dict] = None, metadata: Optional[Dict] = None, + template_dictionary: Optional[Dict] = None, ) -> Tuple[List[Stack], List[str]]: """ Recursively extract stacks from a template file. @@ -205,7 +207,8 @@ def get_stacks( Parameters ---------- template_file: str - the file path of the template to extract stacks from + the file path of the template to extract stacks from. Only one of either template_dict or template_file + is required stack_path: str the stack path of the parent stack, for root stack, it is "" name: str @@ -218,6 +221,8 @@ def get_stacks( that might want to get substituted within the template and its child templates metadata: Optional[Dict] Optional dictionary of nested stack resource metadata values. + template_dictionary: Optional[Dict] + dictionary representing the sam template. Only one of either template_dict or template_file is required Returns ------- @@ -226,7 +231,17 @@ def get_stacks( remote_stack_full_paths : List[str] The list of full paths of detected remote stacks """ - template_dict = get_template_data(template_file) + template_dict: dict + if template_file: + template_dict = get_template_data(template_file) + elif template_dictionary: + template_file = "" + template_dict = template_dictionary + else: + raise TemplateNotFoundException( + message="A template file or a template dict is required but both are missing." + ) + stacks = [ Stack( stack_path, diff --git a/samcli/lib/translate/__init__.py b/samcli/lib/translate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/commands/validate/lib/sam_template_validator.py b/samcli/lib/translate/sam_template_validator.py similarity index 97% rename from samcli/commands/validate/lib/sam_template_validator.py rename to samcli/lib/translate/sam_template_validator.py index 3cb863f634..b7ee8b285d 100644 --- a/samcli/commands/validate/lib/sam_template_validator.py +++ b/samcli/lib/translate/sam_template_validator.py @@ -13,7 +13,7 @@ from samcli.lib.utils.packagetype import ZIP, IMAGE from samcli.lib.utils.resources import AWS_SERVERLESS_FUNCTION from samcli.yamlhelper import yaml_dump -from .exceptions import InvalidSamDocumentException +from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException LOG = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def __init__(self, sam_template, managed_policy_loader, profile=None, region=Non self.sam_parser = parser.Parser() self.boto3_session = Session(profile_name=profile, region_name=region) - def is_valid(self): + def get_translated_template_if_valid(self): """ Runs the SAM Translator to determine if the template provided is valid. This is similar to running a ChangeSet in CloudFormation for a SAM Template @@ -70,6 +70,7 @@ def is_valid(self): try: template = sam_translator.translate(sam_template=self.sam_template, parameter_values={}) LOG.debug("Translated template is:\n%s", yaml_dump(template)) + return yaml_dump(template) except InvalidDocumentException as e: raise InvalidSamDocumentException( functools.reduce(lambda message, error: message + " " + str(error), e.causes, str(e)) diff --git a/samcli/lib/utils/resources.py b/samcli/lib/utils/resources.py index b05565e22f..aed3adc244 100644 --- a/samcli/lib/utils/resources.py +++ b/samcli/lib/utils/resources.py @@ -20,11 +20,15 @@ AWS_APIGATEWAY_RESOURCE = "AWS::ApiGateway::Resource" AWS_APIGATEWAY_METHOD = "AWS::ApiGateway::Method" AWS_APIGATEWAY_DEPLOYMENT = "AWS::ApiGateway::Deployment" +AWS_APIGATEWAY_BASE_PATH_MAPPING = "AWS::ApiGateway::BasePathMapping" +AWS_APIGATWAY_DOMAIN_NAME = "AWS::ApiGateway::DomainName" AWS_APIGATEWAY_V2_API = "AWS::ApiGatewayV2::Api" AWS_APIGATEWAY_V2_INTEGRATION = "AWS::ApiGatewayV2::Integration" AWS_APIGATEWAY_V2_ROUTE = "AWS::ApiGatewayV2::Route" AWS_APIGATEWAY_V2_STAGE = "AWS::ApiGatewayV2::Stage" +AWS_APIGATEWAY_v2_BASE_PATH_MAPPING = "AWS::ApiGatewayV2::ApiMapping" +AWS_APIGATEWAY_V2_DOMAIN_NAME = "AWS::ApiGatewayV2::DomainName" # SFN AWS_SERVERLESS_STATEMACHINE = "AWS::Serverless::StateMachine" @@ -92,6 +96,8 @@ AWS_CLOUDFORMATION_STACK: "TemplateURL", } +AWS_LAMBDA_FUNCTION_URL = "AWS::Lambda::Url" + def get_packageable_resource_paths(): """ diff --git a/tests/functional/commands/validate/lib/test_sam_template_validator.py b/tests/functional/commands/validate/lib/test_sam_template_validator.py index 8d79b836ea..659663b7e6 100644 --- a/tests/functional/commands/validate/lib/test_sam_template_validator.py +++ b/tests/functional/commands/validate/lib/test_sam_template_validator.py @@ -5,7 +5,7 @@ import samcli.yamlhelper as yamlhelper -from samcli.commands.validate.lib.sam_template_validator import SamTemplateValidator +from samcli.lib.translate.sam_template_validator import SamTemplateValidator from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException # Out of TestValidate's scope because https://stackoverflow.com/a/47224266 @@ -39,7 +39,7 @@ def test_valid_template(self): validator = SamTemplateValidator(template, managed_policy_mock, region="us-east-1") # Should not throw an exception - validator.is_valid() + validator.get_translated_template_if_valid() def test_invalid_template(self): template = { @@ -59,7 +59,7 @@ def test_invalid_template(self): validator = SamTemplateValidator(template, managed_policy_mock, region="us-east-1") with self.assertRaises(InvalidSamDocumentException): - validator.is_valid() + validator.get_translated_template_if_valid() def test_valid_template_with_local_code_for_function(self): template = { @@ -79,7 +79,7 @@ def test_valid_template_with_local_code_for_function(self): validator = SamTemplateValidator(template, managed_policy_mock, region="us-east-1") # Should not throw an exception - validator.is_valid() + validator.get_translated_template_if_valid() def test_valid_template_with_local_code_for_layer_version(self): template = { @@ -96,7 +96,7 @@ def test_valid_template_with_local_code_for_layer_version(self): validator = SamTemplateValidator(template, managed_policy_mock, region="us-east-1") # Should not throw an exception - validator.is_valid() + validator.get_translated_template_if_valid() def test_valid_template_with_local_code_for_api(self): template = { @@ -116,7 +116,7 @@ def test_valid_template_with_local_code_for_api(self): validator = SamTemplateValidator(template, managed_policy_mock, region="us-east-1") # Should not throw an exception - validator.is_valid() + validator.get_translated_template_if_valid() def test_valid_template_with_DefinitionBody_for_api(self): template = { @@ -136,7 +136,7 @@ def test_valid_template_with_DefinitionBody_for_api(self): validator = SamTemplateValidator(template, managed_policy_mock, region="us-east-1") # Should not throw an exception - validator.is_valid() + validator.get_translated_template_if_valid() def test_valid_template_with_s3_object_passed(self): template = { @@ -168,7 +168,7 @@ def test_valid_template_with_s3_object_passed(self): validator = SamTemplateValidator(template, managed_policy_mock, region="us-east-1") # Should not throw an exception - validator.is_valid() + validator.get_translated_template_if_valid() # validate the CodeUri was not changed self.assertEqual( @@ -190,4 +190,4 @@ def test_valid_api_request_model_template(self, template_path): validator = SamTemplateValidator(template, managed_policy_mock, region="us-east-1") # Should not throw an exception - validator.is_valid() + validator.get_translated_template_if_valid() diff --git a/tests/integration/list/__init__.py b/tests/integration/list/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/list/endpoints/__init__.py b/tests/integration/list/endpoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/list/endpoints/endpoints_integ_base.py b/tests/integration/list/endpoints/endpoints_integ_base.py new file mode 100644 index 0000000000..a5dc3087af --- /dev/null +++ b/tests/integration/list/endpoints/endpoints_integ_base.py @@ -0,0 +1,27 @@ +from tests.integration.list.list_integ_base import ListIntegBase + + +class EndpointsIntegBase(ListIntegBase): + def get_endpoints_command_list( + self, stack_name=None, output=None, region=None, profile=None, template_file=None, help=False + ): + command_list = [self.base_command(), "list", "endpoints"] + if stack_name: + command_list += ["--stack-name", str(stack_name)] + + if output: + command_list += ["--output", str(output)] + + if region: + command_list += ["--region", str(region)] + + if profile: + command_list += ["--profile", str(profile)] + + if template_file: + command_list += ["--template-file", str(template_file)] + + if help: + command_list += ["--help"] + + return command_list diff --git a/tests/integration/list/endpoints/test_endpoints_command.py b/tests/integration/list/endpoints/test_endpoints_command.py new file mode 100644 index 0000000000..5fc96b56a5 --- /dev/null +++ b/tests/integration/list/endpoints/test_endpoints_command.py @@ -0,0 +1,133 @@ +import os +import time +import boto3 +import re +from unittest import skipIf +from tests.integration.deploy.deploy_integ_base import DeployIntegBase +from tests.integration.list.endpoints.endpoints_integ_base import EndpointsIntegBase +from samcli.commands.list.endpoints.command import HELP_TEXT +from tests.testing_utils import CI_OVERRIDE, RUN_BY_CANARY +from tests.testing_utils import run_command, run_command_with_input, method_to_stack_name + +CFN_SLEEP = 3 +CFN_PYTHON_VERSION_SUFFIX = os.environ.get("PYTHON_VERSION", "0.0.0").replace(".", "-") + + +@skipIf( + (not RUN_BY_CANARY and not CI_OVERRIDE), + "Skip Terraform test cases unless running in CI", +) +class TestEndpoints(DeployIntegBase, EndpointsIntegBase): + @classmethod + def setUpClass(cls): + DeployIntegBase.setUpClass() + EndpointsIntegBase.setUpClass() + + def setUp(self): + self.cf_client = boto3.client("cloudformation") + time.sleep(CFN_SLEEP) + super().setUp() + + def test_endpoints_help_message(self): + cmdlist = self.get_endpoints_command_list(help=True) + command_result = run_command(cmdlist) + from_command = "".join(command_result.stdout.decode().split()) + from_help = "".join(HELP_TEXT.split()) + self.assertIn(from_help, from_command, "Endpoints help text should have been printed") + + def test_no_stack_name(self): + template_path = self.list_test_data_path.joinpath("test_endpoints_template.yaml") + region = boto3.Session().region_name + cmdlist = self.get_endpoints_command_list( + stack_name=None, output="json", region=region, template_file=template_path + ) + command_result = run_command(cmdlist, cwd=self.working_dir) + expected_output = [ + """{ + "LogicalResourceId": "HelloWorldFunction", + "PhysicalResourceId": "-", + "CloudEndpoint": "-", + "Methods": "-" + }""", + """{ + "LogicalResourceId": "TestAPI", + "PhysicalResourceId": "-", + "CloudEndpoint": "-", + "Methods": [] + }""", + """{ + "LogicalResourceId": "ServerlessRestApi", + "PhysicalResourceId": "-", + "CloudEndpoint": "-", + "Methods": [ + "/hello2['get']", + "/hello['get']" + ] + }""", + ] + for expression in expected_output: + self.assertIn(expression, command_result.stdout.decode()) + + def test_has_stack_name(self): + template_path = self.list_test_data_path.joinpath("test_endpoints_template.yaml") + stack_name = method_to_stack_name(self.id()) + region = boto3.Session().region_name + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + guided=True, + region=region, + confirm_changeset=True, + disable_rollback=True, + ) + run_command_with_input( + deploy_command_list, "{}\n{}\nY\nY\nY\nY\nY\nY\n\n\nY\n".format(stack_name, region).encode() + ) + cmdlist = self.get_endpoints_command_list( + stack_name=stack_name, output="json", region=region, template_file=template_path + ) + command_result = run_command(cmdlist, cwd=self.working_dir) + expected_output = [ + """{ + "LogicalResourceId": "HelloWorldFunction", + "PhysicalResourceId": "test-has-stack-name.*", + "CloudEndpoint": "https://.*.lambda-url..*.on.aws/", + "Methods": "-" + }""", + """ { + "LogicalResourceId": "ServerlessRestApi", + "PhysicalResourceId": ".*", + "CloudEndpoint": .* + "https://.*.execute-api..*.amazonaws.com/Prod", + "https://.*.execute-api..*.amazonaws.com/Stage" + .*, + "Methods": .* + "/hello2.'get'.", + "/hello.'get'." + . + }""", + """ { + "LogicalResourceId": "TestAPI", + "PhysicalResourceId": ".*", + "CloudEndpoint": . + "https://.*.execute-api..*.amazonaws.com/Test2" + ., + "Methods": .. + }""", + ] + for expression in expected_output: + self.assertTrue(re.search(expression, command_result.stdout.decode())) + + def test_stack_does_not_exist(self): + template_path = self.list_test_data_path.joinpath("test_endpoints_template.yaml") + stack_name = method_to_stack_name(self.id()) + region = boto3.Session().region_name + cmdlist = self.get_endpoints_command_list( + stack_name=stack_name, output="json", region=region, template_file=template_path + ) + command_result = run_command(cmdlist, cwd=self.working_dir) + expected_output = ( + f"Error: The input stack {stack_name} does" f" not exist on Cloudformation in the region {region}" + ) + self.assertIn( + expected_output, command_result.stderr.decode(), "Should have raised error that outputs do not exist" + ) diff --git a/tests/integration/list/list_integ_base.py b/tests/integration/list/list_integ_base.py new file mode 100644 index 0000000000..e5055e4797 --- /dev/null +++ b/tests/integration/list/list_integ_base.py @@ -0,0 +1,36 @@ +import os +from unittest import TestCase +from pathlib import Path +import uuid +import shutil +import tempfile +from tests.testing_utils import get_sam_command + + +class ListIntegBase(TestCase): + @classmethod + def setUpClass(cls): + cls.cmd = cls.base_command() + cls.list_test_data_path = Path(__file__).resolve().parents[1].joinpath("testdata", "list") + + def setUp(self): + super().setUp() + self.scratch_dir = str(Path(__file__).resolve().parent.joinpath(str(uuid.uuid4()).replace("-", "")[:10])) + shutil.rmtree(self.scratch_dir, ignore_errors=True) + os.mkdir(self.scratch_dir) + self.working_dir = tempfile.mkdtemp(dir=self.scratch_dir) + + def tearDown(self): + super().tearDown() + self.working_dir and shutil.rmtree(self.working_dir, ignore_errors=True) + self.scratch_dir and shutil.rmtree(self.scratch_dir, ignore_errors=True) + self.cleanup_config() + + def cleanup_config(self): + config_path = Path(self.list_test_data_path, "samconfig.toml") + if os.path.exists(config_path): + os.remove(config_path) + + @classmethod + def base_command(cls): + return get_sam_command() diff --git a/tests/integration/list/resources/__init__.py b/tests/integration/list/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/list/resources/resources_integ_base.py b/tests/integration/list/resources/resources_integ_base.py new file mode 100644 index 0000000000..deccf660e2 --- /dev/null +++ b/tests/integration/list/resources/resources_integ_base.py @@ -0,0 +1,27 @@ +from tests.integration.list.list_integ_base import ListIntegBase + + +class ResourcesIntegBase(ListIntegBase): + def get_resources_command_list( + self, stack_name=None, output=None, region=None, profile=None, template_file=None, help=False + ): + command_list = [self.base_command(), "list", "resources"] + if stack_name: + command_list += ["--stack-name", str(stack_name)] + + if output: + command_list += ["--output", str(output)] + + if region: + command_list += ["--region", str(region)] + + if profile: + command_list += ["--profile", str(profile)] + + if template_file: + command_list += ["--template-file", str(template_file)] + + if help: + command_list += ["--help"] + + return command_list diff --git a/tests/integration/list/resources/test_resources_command.py b/tests/integration/list/resources/test_resources_command.py new file mode 100644 index 0000000000..4f1cf8533e --- /dev/null +++ b/tests/integration/list/resources/test_resources_command.py @@ -0,0 +1,123 @@ +import os +import time +import boto3 +import re +from unittest import skipIf +from tests.integration.deploy.deploy_integ_base import DeployIntegBase +from tests.integration.list.resources.resources_integ_base import ResourcesIntegBase +from samcli.commands.list.resources.command import HELP_TEXT +from tests.testing_utils import CI_OVERRIDE, RUN_BY_CANARY +from tests.testing_utils import run_command, run_command_with_input, method_to_stack_name + +CFN_SLEEP = 3 +CFN_PYTHON_VERSION_SUFFIX = os.environ.get("PYTHON_VERSION", "0.0.0").replace(".", "-") + + +@skipIf( + (not RUN_BY_CANARY and not CI_OVERRIDE), + "Skip Terraform test cases unless running in CI", +) +class TestResources(DeployIntegBase, ResourcesIntegBase): + @classmethod + def setUpClass(cls): + DeployIntegBase.setUpClass() + ResourcesIntegBase.setUpClass() + + def setUp(self): + self.cf_client = boto3.client("cloudformation") + time.sleep(CFN_SLEEP) + super().setUp() + + def test_resources_help_message(self): + cmdlist = self.get_resources_command_list(help=True) + command_result = run_command(cmdlist) + from_command = "".join(command_result.stdout.decode().split()) + from_help = "".join(HELP_TEXT.split()) + self.assertIn(from_help, from_command, "Resources help text should have been printed") + + def test_successful_transform(self): + template_path = self.list_test_data_path.joinpath("test_stack_creation_template.yaml") + region = boto3.Session().region_name + cmdlist = self.get_resources_command_list( + stack_name=None, region=region, output="json", template_file=template_path + ) + command_result = run_command(cmdlist, cwd=self.working_dir) + expression_list = [ + """{\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": "-"\n }""", + """{\n "LogicalResourceId": "HelloWorldFunctionRole",\n "PhysicalResourceId": "-"\n }""", + """{\n "LogicalResourceId": "HelloWorldFunctionHelloWorldPermissionProd",\n "PhysicalResourceId": "-"\n }""", + """{\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": "-"\n }""", + """{\n "LogicalResourceId": "ServerlessRestApiProdStage",\n "PhysicalResourceId": "-"\n }""", + ] + for expression in expression_list: + self.assertIn( + expression, + command_result.stdout.decode(), + ) + self.assertTrue( + re.search( + """{\n "LogicalResourceId": "ServerlessRestApiDeployment.*",\n "PhysicalResourceId": "-"\n }""", + command_result.stdout.decode(), + ) + ) + + def test_invalid_template_file(self): + template_path = self.list_test_data_path.joinpath("test_resources_invalid_sam_template.yaml") + region = boto3.Session().region_name + cmdlist = self.get_resources_command_list( + stack_name=None, region=region, output="json", template_file=template_path + ) + command_result = run_command(cmdlist, cwd=self.working_dir) + self.assertIn( + "Error: [InvalidTemplateException(\"'Resources' section is required\")] 'Resources' section is required", + command_result.stderr.decode(), + ) + + def test_success_with_stack_name(self): + template_path = self.list_test_data_path.joinpath("test_stack_creation_template.yaml") + stack_name = method_to_stack_name(self.id()) + region = boto3.Session().region_name + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + guided=True, + region=region, + confirm_changeset=True, + disable_rollback=True, + ) + run_command_with_input( + deploy_command_list, "{}\n{}\nY\nY\nY\nY\nY\n\n\nY\n".format(stack_name, region).encode() + ) + cmdlist = self.get_resources_command_list( + stack_name=stack_name, region=region, output="json", template_file=template_path + ) + command_result = run_command(cmdlist, cwd=self.working_dir) + expression_list = [ + """{\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": ".*HelloWorldFunction.*"\n }""", + """{\n "LogicalResourceId": "HelloWorldFunctionRole",\n "PhysicalResourceId": ".*HelloWorldFunctionRole.*"\n }""", + """{\n "LogicalResourceId": "HelloWorldFunctionHelloWorldPermissionProd",\n "PhysicalResourceId": ".*HelloWorldFunctionHelloWorldPermissionProd.*"\n }""", + """{\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": ".*"\n }""", + """{\n "LogicalResourceId": "ServerlessRestApiProdStage",\n "PhysicalResourceId": ".*"\n }""", + """{\n "LogicalResourceId": "ServerlessRestApiDeployment.*",\n "PhysicalResourceId": ".*"\n }""", + ] + for expression in expression_list: + self.assertTrue( + re.search( + expression, + command_result.stdout.decode(), + ) + ) + + def test_stack_does_not_exist(self): + template_path = self.list_test_data_path.joinpath("test_stack_creation_template.yaml") + stack_name = method_to_stack_name(self.id()) + region = boto3.Session().region_name + cmdlist = self.get_resources_command_list( + stack_name=stack_name, region=region, output="json", template_file=template_path + ) + command_result = run_command(cmdlist, cwd=self.working_dir) + expected_output = ( + f"Error: The input stack {stack_name} does" f" not exist on Cloudformation in the region {region}" + ) + self.assertIn( + expected_output, command_result.stderr.decode(), "Should have raised error that outputs do not exist" + ) diff --git a/tests/integration/list/stack_outputs/__init__.py b/tests/integration/list/stack_outputs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/list/stack_outputs/stack_outputs_integ_base.py b/tests/integration/list/stack_outputs/stack_outputs_integ_base.py new file mode 100644 index 0000000000..8f2db9c863 --- /dev/null +++ b/tests/integration/list/stack_outputs/stack_outputs_integ_base.py @@ -0,0 +1,42 @@ +from tests.integration.list.list_integ_base import ListIntegBase + + +class StackOutputsIntegBase(ListIntegBase): + def get_stack_outputs_command_list(self, stack_name=None, output=None, region=None, profile=None, help=False): + command_list = [self.base_command(), "list", "stack-outputs"] + if stack_name: + command_list += ["--stack-name", str(stack_name)] + + if output: + command_list += ["--output", str(output)] + + if region: + command_list += ["--region", str(region)] + + if profile: + command_list += ["--profile", str(profile)] + + if help: + command_list += ["--help"] + + return command_list + + def check_stack_output(self, output, key=None, value=None, description=None): + if key: + self._check_key(output, key) + if value: + self._check_value(output, value) + if description: + self._check_description(output, description) + + def _check_key(self, output, key): + output_key = output.get("OutputKey") + self.assertEqual(output_key, key) + + def _check_value(self, output, value): + output_value = output.get("OutputValue") + self.assertRegex(output_value, value) + + def _check_description(self, output, description): + output_description = output.get("Description") + self.assertEqual(output_description, description) diff --git a/tests/integration/list/stack_outputs/test_stack_outputs_command.py b/tests/integration/list/stack_outputs/test_stack_outputs_command.py new file mode 100644 index 0000000000..bac508b075 --- /dev/null +++ b/tests/integration/list/stack_outputs/test_stack_outputs_command.py @@ -0,0 +1,109 @@ +import os +import time +import boto3 +import json +from unittest import skipIf + +from tests.integration.deploy.deploy_integ_base import DeployIntegBase +from tests.integration.list.stack_outputs.stack_outputs_integ_base import StackOutputsIntegBase +from samcli.commands.list.stack_outputs.command import HELP_TEXT +from tests.testing_utils import CI_OVERRIDE, RUN_BY_CANARY +from tests.testing_utils import run_command, run_command_with_input, method_to_stack_name + +CFN_SLEEP = 3 +CFN_PYTHON_VERSION_SUFFIX = os.environ.get("PYTHON_VERSION", "0.0.0").replace(".", "-") + + +@skipIf( + (not RUN_BY_CANARY and not CI_OVERRIDE), + "Skip Terraform test cases unless running in CI", +) +class TestStackOutputs(DeployIntegBase, StackOutputsIntegBase): + @classmethod + def setUpClass(cls): + DeployIntegBase.setUpClass() + StackOutputsIntegBase.setUpClass() + + def setUp(self): + self.cf_client = boto3.client("cloudformation") + time.sleep(CFN_SLEEP) + super().setUp() + + def test_stack_outputs_help_message(self): + cmdlist = self.get_stack_outputs_command_list(help=True) + command_result = run_command(cmdlist, cwd=self.working_dir) + from_command = "".join(command_result.stdout.decode().split()) + from_help = "".join(HELP_TEXT.split()) + self.assertIn(from_help, from_command, "Stack-outputs help text should have been printed") + + def test_stack_output_exists(self): + template_path = self.list_test_data_path.joinpath("test_stack_creation_template.yaml") + stack_name = method_to_stack_name(self.id()) + region = boto3.Session().region_name + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + guided=True, + region=region, + confirm_changeset=True, + disable_rollback=True, + ) + run_command_with_input( + deploy_command_list, "{}\n{}\nY\nY\nY\nY\nY\n\n\nY\n".format(stack_name, region).encode() + ) + cmdlist = self.get_stack_outputs_command_list(stack_name=stack_name, region=region, output="json") + command_result = run_command(cmdlist, cwd=self.working_dir) + outputs = json.loads(command_result.stdout.decode()) + self.assertEqual(len(outputs), 3) + self.check_stack_output( + outputs[0], + "HelloWorldFunctionIamRole", + "arn:aws:iam::.*:role/.*-HelloWorldFunctionRole\\-.*", + "Implicit IAM Role created for Hello World function", + ) + self.check_stack_output( + outputs[1], + "HelloWorldApi", + "https://.*execute.*.amazonaws.com/Prod/hello/", + "API Gateway endpoint URL for Prod stage for Hello World function", + ) + self.check_stack_output( + outputs[2], + "HelloWorldFunction", + "arn:aws:lambda:.*:.*:function:.*-HelloWorldFunction\\-.*", + "Hello World Lambda Function ARN", + ) + + def test_stack_no_outputs_exist(self): + template_path = self.list_test_data_path.joinpath("test_stack_no_outputs_template.yaml") + stack_name = method_to_stack_name(self.id()) + region = boto3.Session().region_name + deploy_command_list = self.get_deploy_command_list( + template_file=template_path, + guided=True, + region=region, + confirm_changeset=True, + disable_rollback=True, + ) + run_command_with_input( + deploy_command_list, "{}\n{}\nY\nY\nY\nY\nY\n\n\nY\n".format(stack_name, region).encode() + ) + cmdlist = self.get_stack_outputs_command_list(stack_name=stack_name, region=region, output="json") + command_result = run_command(cmdlist, cwd=self.working_dir) + expected_output = ( + f"Error: Outputs do not exist for the input stack {stack_name}" f" on Cloudformation in the region {region}" + ) + self.assertIn( + expected_output, command_result.stderr.decode(), "Should have raised error that outputs do not exist" + ) + + def test_stack_does_not_exist(self): + stack_name = method_to_stack_name(self.id()) + region = boto3.Session().region_name + cmdlist = self.get_stack_outputs_command_list(stack_name=stack_name, region=region, output="json") + command_result = run_command(cmdlist, cwd=self.working_dir) + expected_output = ( + f"Error: The input stack {stack_name} does" f" not exist on Cloudformation in the region {region}" + ) + self.assertIn( + expected_output, command_result.stderr.decode(), "Should have raised error that outputs do not exist" + ) diff --git a/tests/integration/testdata/list/hello_world/__init__.py b/tests/integration/testdata/list/hello_world/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/list/hello_world/app.py b/tests/integration/testdata/list/hello_world/app.py new file mode 100644 index 0000000000..093062037a --- /dev/null +++ b/tests/integration/testdata/list/hello_world/app.py @@ -0,0 +1,42 @@ +import json + +# import requests + + +def lambda_handler(event, context): + """Sample pure Lambda function + + Parameters + ---------- + event: dict, required + API Gateway Lambda Proxy Input Format + + Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + + context: object, required + Lambda Context runtime methods and attributes + + Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + + Returns + ------ + API Gateway Lambda Proxy Output Format: dict + + Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + """ + + # try: + # ip = requests.get("http://checkip.amazonaws.com/") + # except requests.RequestException as e: + # # Send some context about this error to Lambda Logs + # print(e) + + # raise e + + return { + "statusCode": 200, + "body": json.dumps({ + "message": "hello world", + # "location": ip.text.replace("\n", "") + }), + } diff --git a/tests/integration/testdata/list/hello_world/requirements.txt b/tests/integration/testdata/list/hello_world/requirements.txt new file mode 100644 index 0000000000..663bd1f6a2 --- /dev/null +++ b/tests/integration/testdata/list/hello_world/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/tests/integration/testdata/list/test_endpoints_template.yaml b/tests/integration/testdata/list/test_endpoints_template.yaml new file mode 100644 index 0000000000..7d8a81fec7 --- /dev/null +++ b/tests/integration/testdata/list/test_endpoints_template.yaml @@ -0,0 +1,44 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app-hello + + Sample SAM Template for sam-app-hello + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 3 + Tracing: Active + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler + Runtime: python3.8 + FunctionUrlConfig: + AuthType: AWS_IAM + Architectures: + - x86_64 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + HelloWorld2: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello2 + Method: get + TestAPI: + Type: AWS::Serverless::HttpApi + Properties: + Description: "Test resources" + StageName: Test2 + + + + diff --git a/tests/integration/testdata/list/test_resources_invalid_sam_template.yaml b/tests/integration/testdata/list/test_resources_invalid_sam_template.yaml new file mode 100644 index 0000000000..66a5842012 --- /dev/null +++ b/tests/integration/testdata/list/test_resources_invalid_sam_template.yaml @@ -0,0 +1,42 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app-hello + + Sample SAM Template for sam-app-hello + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 3 + Tracing: Active + +ResourcesMissing: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler + Runtime: python3.8 + Architectures: + - x86_64 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn \ No newline at end of file diff --git a/tests/integration/testdata/list/test_stack_creation_template.yaml b/tests/integration/testdata/list/test_stack_creation_template.yaml new file mode 100644 index 0000000000..5a2efcdd33 --- /dev/null +++ b/tests/integration/testdata/list/test_stack_creation_template.yaml @@ -0,0 +1,39 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: "Test stack for testing sam list" + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 3 + Tracing: Active + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler + Runtime: python3.8 + Architectures: + - x86_64 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/tests/integration/testdata/list/test_stack_no_outputs_template.yaml b/tests/integration/testdata/list/test_stack_no_outputs_template.yaml new file mode 100644 index 0000000000..1f64430eba --- /dev/null +++ b/tests/integration/testdata/list/test_stack_no_outputs_template.yaml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: "Test stack for testing sam list" + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 3 + Tracing: Active + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: hello_world/ + Handler: app.lambda_handler + Runtime: python3.8 + Architectures: + - x86_64 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + diff --git a/tests/unit/commands/list/__init__.py b/tests/unit/commands/list/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/commands/list/endpoints/__init__.py b/tests/unit/commands/list/endpoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/commands/list/endpoints/test_cli.py b/tests/unit/commands/list/endpoints/test_cli.py new file mode 100644 index 0000000000..3ff7c8ac4b --- /dev/null +++ b/tests/unit/commands/list/endpoints/test_cli.py @@ -0,0 +1,36 @@ +from unittest import TestCase +from unittest.mock import Mock, patch +from samcli.commands.list.endpoints.command import do_cli + + +class TestCli(TestCase): + def setUp(self): + self.stack_name = "stack-name" + self.output = "json" + self.region = None + self.profile = None + self.template_file = None + + @patch("samcli.commands.list.endpoints.command.click") + @patch("samcli.commands.list.endpoints.endpoints_context.EndpointsContext") + def test_cli_base_command(self, mock_endpoints_context, mock_endpoints_click): + context_mock = Mock() + mock_endpoints_context.return_value.__enter__.return_value = context_mock + do_cli( + stack_name=self.stack_name, + output=self.output, + region=self.region, + profile=self.profile, + template_file=self.template_file, + ) + + mock_endpoints_context.assert_called_with( + stack_name=self.stack_name, + output=self.output, + region=self.region, + profile=self.profile, + template_file=self.template_file, + ) + + context_mock.run.assert_called_with() + self.assertEqual(context_mock.run.call_count, 1) diff --git a/tests/unit/commands/list/endpoints/test_endpoints_context.py b/tests/unit/commands/list/endpoints/test_endpoints_context.py new file mode 100644 index 0000000000..55daa4bee0 --- /dev/null +++ b/tests/unit/commands/list/endpoints/test_endpoints_context.py @@ -0,0 +1,987 @@ +from unittest import TestCase +from unittest.mock import patch, call, Mock +from botocore.exceptions import ClientError, EndpointConnectionError, NoCredentialsError, BotoCoreError + +from samcli.commands.list.endpoints.endpoints_context import EndpointsContext +from samcli.commands.list.exceptions import ( + SamListLocalResourcesNotFoundError, + SamListUnknownClientError, + SamListUnknownBotoCoreError, +) +from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider +from samcli.lib.list.endpoints.endpoints_producer import EndpointsProducer, APIGatewayEnum +from samcli.lib.list.data_to_json_mapper import DataToJsonMapper +from samcli.commands.list.json_consumer import StringConsumerJsonOutput + + +TRANSLATED_DICT_RETURN = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app-hello\nSample SAM Template for sam-app-hello\n", + "Resources": { + "HelloWorldFunction": { + "Properties": { + "Architectures": ["x86_64"], + "Code": {"S3Bucket": "bucket", "S3Key": "value"}, + "Handler": "app.lambda_handler", + "Role": {"Fn::GetAtt": ["HelloWorldFunctionRole", "Arn"]}, + "Runtime": "python3.8", + "Tags": [{"Key": "lambda:createdBy", "Value": "SAM"}], + "Timeout": 3, + "TracingConfig": {"Mode": "Active"}, + }, + "Type": "AWS::Lambda::Function", + }, + "HelloWorldFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": ["sts:AssumeRole"], + "Effect": "Allow", + "Principal": {"Service": ["lambda.amazonaws.com"]}, + } + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess", + ], + "Tags": [{"Key": "lambda:createdBy", "Value": "SAM"}], + }, + "Type": "AWS::IAM::Role", + }, + "HelloWorldFunctionHelloWorldPermissionProd": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": {"Ref": "HelloWorldFunction"}, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/hello", + {"__ApiId__": {"Ref": "ServerlessRestApi"}, "__Stage__": "*"}, + ] + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ServerlessRestApi": { + "Properties": { + "Body": { + "info": {"version": "1.0", "title": {"Ref": "AWS::StackName"}}, + "paths": { + "/hello": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations" + }, + }, + "responses": {}, + } + } + }, + "swagger": "2.0", + } + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "ServerlessRestApiDeploymentf5716dc08b": { + "Properties": { + "Description": "RestApi deployment id: f5716dc08b0d213bd0f2dfb686579c351b09ae49", + "RestApiId": {"Ref": "ServerlessRestApi"}, + "StageName": "Stage", + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "ServerlessRestApiProdStage": { + "Properties": { + "DeploymentId": {"Ref": "ServerlessRestApiDeploymentf5716dc08b"}, + "RestApiId": {"Ref": "ServerlessRestApi"}, + "StageName": "Prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + }, +} + +TRANSLATED_DICT_RETURN_WITH_APIS = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app-hello\nSample SAM Template for sam-app-hello\n", + "Resources": { + "customDomainCert": { + "Type": "AWS::CertificateManager::Certificate", + "Properties": {"DomainName": "api7.zhandr.people.aws.dev", "ValidationMethod": "DNS"}, + }, + "BPMapping1": { + "Type": "AWS::ApiGateway::BasePathMapping", + "Properties": {"DomainName": "apigw_dm_mapping_LID", "RestApiId": "test_apigw_restapi", "Stage": "String"}, + }, + "HelloWorldFunction": { + "Properties": { + "Architectures": ["x86_64"], + "Code": {"S3Bucket": "bucket", "S3Key": "value"}, + "Handler": "app.lambda_handler", + "Role": {"Fn::GetAtt": ["HelloWorldFunctionRole", "Arn"]}, + "Runtime": "python3.8", + "Tags": [{"Key": "lambda:createdBy", "Value": "SAM"}], + "Timeout": 3, + "TracingConfig": {"Mode": "Active"}, + }, + "Type": "AWS::Lambda::Function", + }, + "HelloWorldFunctionUrl": { + "Properties": {"AuthType": "AWS_IAM", "TargetFunctionArn": {"Ref": "HelloWorldFunction"}}, + "Type": "AWS::Lambda::Url", + }, + "HelloWorldFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": ["sts:AssumeRole"], + "Effect": "Allow", + "Principal": {"Service": ["lambda.amazonaws.com"]}, + } + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess", + ], + "Tags": [{"Key": "lambda:createdBy", "Value": "SAM"}], + }, + "Type": "AWS::IAM::Role", + }, + "HelloWorldFunctionHelloWorldPermissionProd": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": {"Ref": "HelloWorldFunction"}, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/hello", + {"__ApiId__": {"Ref": "ServerlessRestApi"}, "__Stage__": "*"}, + ] + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "HelloWorldFunctionHelloWorld2PermissionProd": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": {"Ref": "HelloWorldFunction"}, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET, PUT/hello2", + {"__ApiId__": {"Ref": "ServerlessRestApi"}, "__Stage__": "*"}, + ] + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "TestResource2": { + "Properties": { + "Body": { + "info": {"version": "1.0", "description": "Test resources", "title": {"Ref": "AWS::StackName"}}, + "paths": {}, + "openapi": "3.0.1", + "tags": [{"name": "httpapi:createdBy", "x-amazon-apigateway-tag-value": "SAM"}], + } + }, + "Type": "AWS::ApiGatewayV2::Api", + }, + "TestResource5": { + "Properties": { + "Body": { + "info": {"version": "1.0", "description": "Test resources", "title": {"Ref": "AWS::StackName"}}, + "paths": {}, + "openapi": "3.0.1", + "tags": [{"name": "httpapi:createdBy", "x-amazon-apigateway-tag-value": "SAM"}], + } + }, + "Type": "AWS::ApiGatewayV2::Api", + }, + "ApiGatewayDomainNameV28437445d28": { + "Properties": { + "DomainName": "api7.zhandr.people.aws.dev", + "DomainNameConfigurations": [ + {"CertificateArn": {"Ref": "customDomainCert"}, "EndpointType": "REGIONAL"} + ], + "Tags": {"httpapi:createdBy": "SAM"}, + }, + "Type": "AWS::ApiGatewayV2::DomainName", + }, + "TestResource2ApiMapping": { + "Properties": { + "ApiId": {"Ref": "TestResource2"}, + "DomainName": {"Ref": "ApiGatewayDomainNameV28437445d28"}, + "Stage": {"Ref": "TestResource2Test2Stage"}, + }, + "Type": "AWS::ApiGatewayV2::ApiMapping", + }, + "TestResource2Test2Stage": { + "Properties": { + "ApiId": {"Ref": "TestResource2"}, + "AutoDeploy": True, + "StageName": "Test2", + "Tags": {"httpapi:createdBy": "SAM"}, + }, + "Type": "AWS::ApiGatewayV2::Stage", + }, + "TestResource4": { + "Properties": { + "Body": { + "info": {"version": "1.0", "description": "Test resources", "title": {"Ref": "AWS::StackName"}}, + "paths": {}, + "openapi": "3.0.1", + "tags": [{"name": "httpapi:createdBy", "x-amazon-apigateway-tag-value": "SAM"}], + } + }, + "Type": "AWS::ApiGatewayV2::Api", + }, + "TestResource4Test2Stage": { + "Properties": { + "ApiId": {"Ref": "TestResource4"}, + "AutoDeploy": True, + "StageName": "Test2", + "Tags": {"httpapi:createdBy": "SAM"}, + }, + "Type": "AWS::ApiGatewayV2::Stage", + }, + "ServerlessRestApi": { + "Properties": { + "Body": { + "info": {"version": "1.0", "title": {"Ref": "AWS::StackName"}}, + "paths": { + "/hello2": { + "get, put": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations" + }, + }, + "responses": {}, + } + }, + "/hello": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations" + }, + }, + "responses": {}, + } + }, + }, + "swagger": "2.0", + } + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "ServerlessRestApiDeployment88d73b1fc4": { + "Properties": { + "Description": "RestApi deployment id: 88d73b1fc436b53afc5f54ce63096d44e97b741b", + "RestApiId": {"Ref": "ServerlessRestApi"}, + "StageName": "Stage", + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "ServerlessRestApiProdStage": { + "Properties": { + "DeploymentId": {"Ref": "ServerlessRestApiDeployment88d73b1fc4"}, + "RestApiId": {"Ref": "ServerlessRestApi"}, + "StageName": "Prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + }, +} + +SAM_APP_HELLO_RETURN_RESPONSE = { + "StackResources": [ + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "ApiGatewayDomainName1", + "PhysicalResourceId": "test.custom.domain1", + "ResourceType": "AWS::ApiGatewayV2::DomainName", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "HelloWorldFunction", + "PhysicalResourceId": "sam-app-hello6-HelloWorldFunction-testID", + "ResourceType": "AWS::Lambda::Function", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "HelloWorldFunctionUrl", + "PhysicalResourceId": "arn:aws:lambda:us-east-1:function:sam-app-hello6-HelloWorldFunction-testID", + "ResourceType": "AWS::Lambda::Url", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "ServerlessRestApi", + "PhysicalResourceId": "jwompba769", + "ResourceType": "AWS::ApiGateway::RestApi", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "ServerlessRestApiDeployment78c5316093", + "PhysicalResourceId": "lulx9h", + "ResourceType": "AWS::ApiGateway::Deployment", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "ServerlessRestApiProdStage", + "PhysicalResourceId": "Prod", + "ResourceType": "AWS::ApiGateway::Stage", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "TestResource2", + "PhysicalResourceId": "erj31jdyw5", + "ResourceType": "AWS::ApiGatewayV2::Api", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "TestResource2ApiMapping", + "PhysicalResourceId": "rut5pp", + "ResourceType": "AWS::ApiGatewayV2::ApiMapping", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "TestResource2Test2Stage", + "PhysicalResourceId": "Test2", + "ResourceType": "AWS::ApiGatewayV2::Stage", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "TestResource4", + "PhysicalResourceId": "5u9ekr1d32", + "ResourceType": "AWS::ApiGatewayV2::Api", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "TestResource4Test2Stage", + "PhysicalResourceId": "Test2", + "ResourceType": "AWS::ApiGatewayV2::Stage", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "customDomainCert", + "PhysicalResourceId": "arn:aws:acm:us-east-1:certificate", + "ResourceType": "AWS::CertificateManager::Certificate", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "test_apigw_restapi", + "PhysicalResourceId": "testPID", + "ResourceType": "AWS::ApiGateway::RestApi", + "ResourceStatus": "CREATE_COMPLETE", + "DriftInformation": {"StackResourceDriftStatus": "NOT_CHECKED"}, + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "apigw_dm_mapping_LID", + "PhysicalResourceId": "test.custom.bpmapping.domain", + "ResourceType": "AWS::ApiGateway::DomainName", + }, + { + "StackName": "sam-app-hello6", + "LogicalResourceId": "BPMapping1", + "PhysicalResourceId": "bp_mapping_PID", + "ResourceType": "AWS::ApiGateway::BasePathMapping", + }, + ], + "ResponseMetadata": { + "RequestId": "b15914d5-009b-46ce-aab8-458efc09f34d", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "x-amzn-requestid": "b15914d5-009b-46ce-aab8-458efc09f34d", + "content-type": "text/xml", + "content-length": "10370", + "date": "Mon, 25 Jul 2022 20:27:05 GMT", + }, + "RetryAttempts": 0, + }, +} + +SAM_FILE_READER_RETURN = { + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "sam-app-hello\nSample SAM Template for sam-app-hello\n", + "Globals": {"Function": {"Tracing": "Active", "Timeout": 3}}, + "Resources": { + "HelloWorldFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "CodeUri": "hello_world/", + "Handler": "app.lambda_handler", + "Architectures": ["x86_64"], + "Runtime": "python3.8", + "Events": {"HelloWorld": {"Type": "Api", "Properties": {"Path": "/hello", "Method": "get"}}}, + }, + } + }, +} + + +class TestEndpointsInitClients(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("boto3.Session.region_name", "us-east-1") + def test_init_clients_no_input_region_get_region_from_session( + self, patched_click_get_current_context, patched_click_echo + ): + with EndpointsContext( + stack_name="test", output="json", region=None, profile=None, template_file=None + ) as endpoints_context: + endpoints_context.init_clients() + self.assertEqual(endpoints_context.region, "us-east-1") + + +class TestGetFunctionUrl(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_clienterror_resource_not_found( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_resource.side_effect = ClientError( + {"Error": {"Code": "ResourceNotFoundException", "Message": "The resource you requested does not exist"}}, + "GetResources", + ) + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=mock_client_provider.return_value.return_value, + apigateway_client=None, + apigatewayv2_client=None, + mapper=None, + consumer=None, + ) + response = endpoint_producer.get_function_url("testID") + self.assertEqual(response, "-") + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_clienterror_others( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_resource.side_effect = ClientError( + {"Error": {"Code": "ExpiredToken", "Message": "The security token included in the request is expired"}}, + "DescribeStacks", + ) + with self.assertRaises(SamListUnknownClientError): + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=mock_client_provider.return_value.return_value, + apigateway_client=None, + apigatewayv2_client=None, + mapper=None, + consumer=None, + ) + endpoint_producer.get_function_url("testID") + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_properties_not_in_response( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_resource.return_value = {} + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=mock_client_provider.return_value.return_value, + apigateway_client=None, + apigatewayv2_client=None, + mapper=None, + consumer=None, + ) + response = endpoint_producer.get_function_url("testID") + self.assertEqual(response, "-") + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_properties_in_response( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_resource.return_value = { + "TypeName": "AWS::Lambda::Url", + "ResourceDescription": { + "Identifier": "testid", + "Properties": '{"FunctionArn":"arn:aws:lambda:sam-app-hello-HelloWorldFunction","FunctionUrl":"https://test.lambda-url.us-east-1.on.aws/","AuthType":"AWS_IAM"}', + }, + "ResponseMetadata": { + "RequestId": "testID", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "x-amzn-requestid": "testID", + "date": "testDate", + "content-type": "application/x-amz-json-1.0", + "content-length": "408", + }, + "RetryAttempts": 0, + }, + } + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=mock_client_provider.return_value.return_value, + apigateway_client=None, + apigatewayv2_client=None, + mapper=None, + consumer=None, + ) + response = endpoint_producer.get_function_url("testID") + self.assertEqual(response, "https://test.lambda-url.us-east-1.on.aws/") + + +class TestGetStages(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_apigw_v2_stages( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_stages.return_value = { + "ResponseMetadata": { + "RequestId": "testid", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "date": "Mon, 18 Jul 2022 20:59:15 GMT", + "content-type": "application/json", + "content-length": "762", + "connection": "keep-alive", + "x-amzn-requestid": "testid", + "access-control-allow-origin": "*", + "x-amz-apigw-id": "testid", + "access-control-expose-headers": "x-amzn-RequestId,x-amzn-ErrorType,x-amzn-ErrorMessage,Date", + "x-amzn-trace-id": "Root=testid", + }, + "RetryAttempts": 0, + }, + "Items": [ + { + "AutoDeploy": True, + "DefaultRouteSettings": {"DetailedMetricsEnabled": False}, + "RouteSettings": {}, + "StageName": "$default", + "StageVariables": {}, + } + ], + } + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=None, + apigatewayv2_client=mock_client_provider.return_value.return_value, + mapper=None, + consumer=None, + ) + response = endpoint_producer.get_stage_list("testID", APIGatewayEnum.API_GATEWAY_V2) + self.assertEqual(response, ["$default"]) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_apigw_stages( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_stages.return_value = { + "ResponseMetadata": { + "RequestId": "testID", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "date": "Mon, 18 Jul 2022 21:15:06 GMT", + "content-type": "application/json", + "content-length": "679", + "connection": "keep-alive", + "x-amzn-requestid": "testID", + "x-amz-apigw-id": "testID", + }, + "RetryAttempts": 0, + }, + "item": [ + { + "deploymentId": "t50nmu", + "stageName": "Prod", + "cacheClusterEnabled": False, + "cacheClusterStatus": "NOT_AVAILABLE", + "methodSettings": {}, + "tracingEnabled": False, + "tags": {"aws:cloudformation:logical-id": "testID", "aws:cloudformation:stack-name": "testStack"}, + }, + { + "deploymentId": "t50nmu", + "stageName": "Stage", + "cacheClusterEnabled": False, + "cacheClusterStatus": "NOT_AVAILABLE", + "methodSettings": {}, + "tracingEnabled": False, + }, + ], + } + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=mock_client_provider.return_value.return_value, + apigatewayv2_client=None, + mapper=None, + consumer=None, + ) + response = endpoint_producer.get_stage_list("testID", APIGatewayEnum.API_GATEWAY) + self.assertEqual(response, ["Prod", "Stage"]) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_apigw_stages_empty_return( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_stages.return_value = { + "ResponseMetadata": { + "RequestId": "testID", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "date": "Mon, 18 Jul 2022 21:15:06 GMT", + "content-type": "application/json", + "content-length": "679", + "connection": "keep-alive", + "x-amzn-requestid": "testID", + "x-amz-apigw-id": "testID", + }, + "RetryAttempts": 0, + }, + } + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=mock_client_provider.return_value.return_value, + apigatewayv2_client=None, + mapper=None, + consumer=None, + ) + response = endpoint_producer.get_stage_list("testID", APIGatewayEnum.API_GATEWAY) + self.assertEqual(response, []) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_get_stage_list_unknown_clienterror( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_stages.side_effect = ClientError( + {"Error": {"Code": "ExpiredToken", "Message": "The security token included in the request is expired"}}, + "DescribeStacks", + ) + with self.assertRaises(SamListUnknownClientError): + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=mock_client_provider.return_value.return_value, + apigatewayv2_client=mock_client_provider.return_value.return_value, + mapper=None, + consumer=None, + ) + endpoint_producer.get_stage_list("testID", APIGatewayEnum.API_GATEWAY) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_get_stage_list_not_found_exception_clienterror( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_stages.side_effect = ClientError( + {"Error": {"Code": "NotFoundException", "Message": ""}}, + "DescribeStacks", + ) + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=mock_client_provider.return_value.return_value, + apigatewayv2_client=mock_client_provider.return_value.return_value, + mapper=None, + consumer=None, + ) + response = endpoint_producer.get_stage_list("testID", APIGatewayEnum.API_GATEWAY) + self.assertEqual(response, []) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_get_stage_list_unknown_botocore_error( + self, + mock_client_provider, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.get_stages.side_effect = EndpointConnectionError( + endpoint_url="https://cloudformation.test.amazonaws.com/" + ) + with self.assertRaises(SamListUnknownBotoCoreError): + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=mock_client_provider.return_value.return_value, + apigatewayv2_client=mock_client_provider.return_value.return_value, + mapper=None, + consumer=None, + ) + endpoint_producer.get_stage_list("testID", APIGatewayEnum.API_GATEWAY) + + +class TestBuildAPIGWEndpoints(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + def test_build_api_gw_endpoints( + self, + patched_click_get_current_context, + patched_click_echo, + ): + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=None, + apigatewayv2_client=None, + mapper=None, + consumer=None, + ) + repsonse1 = endpoint_producer.build_api_gw_endpoints("testID", []) + self.assertEqual(repsonse1, []) + repsonse2 = endpoint_producer.build_api_gw_endpoints("testID", ["Prod"]) + self.assertEqual(repsonse2, ["https://testID.execute-api.us-east-1.amazonaws.com/Prod"]) + + +class TestEndpointsProducerProduce(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.endpoints.endpoints_producer.SamLocalStackProvider.get_stacks") + @patch("samcli.lib.list.endpoints.endpoints_producer.get_template_data") + @patch("samcli.lib.list.endpoints.endpoints_producer.EndpointsProducer.get_translated_dict") + def test_produce_resources_not_found_error( + self, + mock_get_translated_dict, + mock_get_template_data, + mock_get_stacks, + patched_click_get_current_context, + patched_click_echo, + ): + mock_get_template_data.return_value = {} + mock_get_translated_dict.return_value = {} + mock_get_stacks.return_value = ([], []) + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=None, + apigatewayv2_client=None, + mapper=None, + consumer=None, + ) + with self.assertRaises(SamListLocalResourcesNotFoundError): + endpoint_producer.produce() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.endpoints.endpoints_producer.get_template_data") + @patch("samcli.lib.list.endpoints.endpoints_producer.EndpointsProducer.get_translated_dict") + def test_produce_no_stack_name_json( + self, + mock_get_translated_dict, + mock_get_template_data, + patched_click_get_current_context, + patched_click_echo, + ): + mock_get_template_data.return_value = {} + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN_WITH_APIS + + stacks = SamLocalStackProvider.get_stacks( + template_file="", template_dictionary=mock_get_translated_dict.return_value + ) + endpoint_producer = EndpointsProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=None, + apigatewayv2_client=None, + mapper=DataToJsonMapper(), + consumer=StringConsumerJsonOutput(), + ) + endpoint_producer.produce() + expected_output = [ + call( + '[\n {\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": "-",\n "CloudEndpoint": "-",\n "Methods": "-"\n },\n {\n "LogicalResourceId": "TestResource2",\n "PhysicalResourceId": "-",\n "CloudEndpoint": "-",\n "Methods": []\n },\n {\n "LogicalResourceId": "TestResource5",\n "PhysicalResourceId": "-",\n "CloudEndpoint": "-",\n "Methods": []\n },\n {\n "LogicalResourceId": "TestResource4",\n "PhysicalResourceId": "-",\n "CloudEndpoint": "-",\n "Methods": []\n },\n {\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": "-",\n "CloudEndpoint": "-",\n "Methods": [\n "/hello2[\'get, put\']",\n "/hello[\'get\']"\n ]\n }\n]' + ) + ] + self.assertEqual(patched_click_echo.call_args_list, expected_output) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.endpoints.endpoints_producer.get_template_data") + @patch("samcli.lib.list.endpoints.endpoints_producer.EndpointsProducer.get_translated_dict") + @patch("samcli.lib.list.endpoints.endpoints_producer.EndpointsProducer.get_resources_info") + @patch("samcli.lib.list.endpoints.endpoints_producer.EndpointsProducer.get_function_url") + @patch("samcli.lib.list.endpoints.endpoints_producer.EndpointsProducer.get_stage_list") + def test_produce_has_stack_name_( + self, + mock_get_stages_list, + mock_get_function_url, + mock_get_resources_info, + mock_get_translated_dict, + mock_get_template_data, + patched_click_get_current_context, + patched_click_echo, + ): + mock_get_stages_list.return_value = ["testStage"] + mock_get_function_url.return_value = "test.function.url" + mock_get_resources_info.return_value = SAM_APP_HELLO_RETURN_RESPONSE + mock_get_template_data.return_value = {} + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN_WITH_APIS + + stacks = SamLocalStackProvider.get_stacks( + template_file="", template_dictionary=mock_get_translated_dict.return_value + ) + endpoint_producer = EndpointsProducer( + stack_name="sam-app-hello6", + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + cloudcontrol_client=None, + apigateway_client=None, + apigatewayv2_client=None, + mapper=DataToJsonMapper(), + consumer=StringConsumerJsonOutput(), + ) + endpoint_producer.produce() + expected_output = [ + call( + '[\n {\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": "sam-app-hello6-HelloWorldFunction-testID",\n "CloudEndpoint": "test.function.url",\n "Methods": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": "jwompba769",\n "CloudEndpoint": [\n "https://jwompba769.execute-api.us-east-1.amazonaws.com/testStage"\n ],\n "Methods": [\n "/hello2[\'get, put\']",\n "/hello[\'get\']"\n ]\n },\n {\n "LogicalResourceId": "TestResource2",\n "PhysicalResourceId": "erj31jdyw5",\n "CloudEndpoint": [\n "https://erj31jdyw5.execute-api.us-east-1.amazonaws.com/testStage"\n ],\n "Methods": []\n },\n {\n "LogicalResourceId": "TestResource4",\n "PhysicalResourceId": "5u9ekr1d32",\n "CloudEndpoint": [\n "https://5u9ekr1d32.execute-api.us-east-1.amazonaws.com/testStage"\n ],\n "Methods": []\n },\n {\n "LogicalResourceId": "test_apigw_restapi",\n "PhysicalResourceId": "testPID",\n "CloudEndpoint": [\n "https://test.custom.bpmapping.domain"\n ],\n "Methods": []\n },\n {\n "LogicalResourceId": "TestResource5",\n "PhysicalResourceId": "-",\n "CloudEndpoint": "-",\n "Methods": []\n }\n]' + ) + ] + self.assertEqual(patched_click_echo.call_args_list, expected_output) diff --git a/tests/unit/commands/list/resources/__init__.py b/tests/unit/commands/list/resources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/commands/list/resources/test_cli.py b/tests/unit/commands/list/resources/test_cli.py new file mode 100644 index 0000000000..57dea557d3 --- /dev/null +++ b/tests/unit/commands/list/resources/test_cli.py @@ -0,0 +1,36 @@ +from unittest import TestCase +from unittest.mock import Mock, patch +from samcli.commands.list.resources.command import do_cli + + +class TestCli(TestCase): + def setUp(self): + self.stack_name = "stack-name" + self.output = "json" + self.region = None + self.profile = None + self.template_file = None + + @patch("samcli.commands.list.resources.command.click") + @patch("samcli.commands.list.resources.resources_context.ResourcesContext") + def test_cli_base_command(self, mock_resources_context, mock_resources_click): + context_mock = Mock() + mock_resources_context.return_value.__enter__.return_value = context_mock + do_cli( + stack_name=self.stack_name, + output=self.output, + region=self.region, + profile=self.profile, + template_file=self.template_file, + ) + + mock_resources_context.assert_called_with( + stack_name=self.stack_name, + output=self.output, + region=self.region, + profile=self.profile, + template_file=self.template_file, + ) + + context_mock.run.assert_called_with() + self.assertEqual(context_mock.run.call_count, 1) diff --git a/tests/unit/commands/list/resources/test_resources_context.py b/tests/unit/commands/list/resources/test_resources_context.py new file mode 100644 index 0000000000..2d67053f37 --- /dev/null +++ b/tests/unit/commands/list/resources/test_resources_context.py @@ -0,0 +1,591 @@ +from unittest import TestCase + +from samtranslator.model.exceptions import ExceptionWithMessage +from unittest.mock import patch, call, Mock +from botocore.exceptions import ClientError, EndpointConnectionError, NoCredentialsError, BotoCoreError +from samtranslator.translator.arn_generator import NoRegionFound + +from samcli.commands.list.resources.resources_context import ResourcesContext +from samcli.commands.local.cli_common.user_exceptions import InvalidSamTemplateException +from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException +from samcli.commands.exceptions import RegionError, UserException +from samcli.commands.list.exceptions import ( + SamListLocalResourcesNotFoundError, + SamListUnknownClientError, + StackDoesNotExistInRegionError, + SamListUnknownBotoCoreError, +) +from samtranslator.public.exceptions import InvalidDocumentException +from samcli.lib.translate.sam_template_validator import SamTemplateValidator +from samcli.lib.list.resources.resource_mapping_producer import ResourceMappingProducer +from samcli.lib.list.data_to_json_mapper import DataToJsonMapper +from samcli.commands.list.json_consumer import StringConsumerJsonOutput + + +TRANSLATED_DICT_RETURN = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app-hello\nSample SAM Template for sam-app-hello\n", + "Resources": { + "HelloWorldFunction": { + "Properties": { + "Architectures": ["x86_64"], + "Code": {"S3Bucket": "bucket", "S3Key": "value"}, + "Handler": "app.lambda_handler", + "Role": {"Fn::GetAtt": ["HelloWorldFunctionRole", "Arn"]}, + "Runtime": "python3.8", + "Tags": [{"Key": "lambda:createdBy", "Value": "SAM"}], + "Timeout": 3, + "TracingConfig": {"Mode": "Active"}, + }, + "Type": "AWS::Lambda::Function", + }, + "HelloWorldFunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": ["sts:AssumeRole"], + "Effect": "Allow", + "Principal": {"Service": ["lambda.amazonaws.com"]}, + } + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess", + ], + "Tags": [{"Key": "lambda:createdBy", "Value": "SAM"}], + }, + "Type": "AWS::IAM::Role", + }, + "HelloWorldFunctionHelloWorldPermissionProd": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": {"Ref": "HelloWorldFunction"}, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/hello", + {"__ApiId__": {"Ref": "ServerlessRestApi"}, "__Stage__": "*"}, + ] + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ServerlessRestApi": { + "Properties": { + "Body": { + "info": {"version": "1.0", "title": {"Ref": "AWS::StackName"}}, + "paths": { + "/hello": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations" + }, + }, + "responses": {}, + } + } + }, + "swagger": "2.0", + } + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "ServerlessRestApiDeploymentf5716dc08b": { + "Properties": { + "Description": "RestApi deployment id: f5716dc08b0d213bd0f2dfb686579c351b09ae49", + "RestApiId": {"Ref": "ServerlessRestApi"}, + "StageName": "Stage", + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "ServerlessRestApiProdStage": { + "Properties": { + "DeploymentId": {"Ref": "ServerlessRestApiDeploymentf5716dc08b"}, + "RestApiId": {"Ref": "ServerlessRestApi"}, + "StageName": "Prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + }, +} + +SAM_FILE_READER_RETURN = { + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "sam-app-hello\nSample SAM Template for sam-app-hello\n", + "Globals": {"Function": {"Tracing": "Active", "Timeout": 3}}, + "Resources": { + "HelloWorldFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "CodeUri": "hello_world/", + "Handler": "app.lambda_handler", + "Architectures": ["x86_64"], + "Runtime": "python3.8", + "Events": {"HelloWorld": {"Type": "Api", "Properties": {"Path": "/hello", "Method": "get"}}}, + }, + } + }, +} + + +class TestResourcesContext(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + def test_resources_context_run_local_only_no_stack_name( + self, mock_get_translated_dict, mock_sam_file_reader, patched_click_get_current_context, patched_click_echo + ): + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + with ResourcesContext( + stack_name=None, output="json", region="us-east-1", profile=None, template_file=None + ) as resources_context: + resources_context.run() + expected_output = [ + call( + '[\n {\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "HelloWorldFunctionRole",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "HelloWorldFunctionHelloWorldPermissionProd",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApiDeploymentf5716dc08b",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApiProdStage",\n "PhysicalResourceId": "-"\n }\n]' + ) + ] + print(patched_click_echo.call_args_list) + self.assertEqual(expected_output, patched_click_echo.call_args_list) + + +class TestResourceMappingProducerProduce(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + def test_resources_local_only_no_stack_name( + self, mock_get_translated_dict, mock_sam_file_reader, patched_click_get_current_context, patched_click_echo + ): + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + resource_producer = ResourceMappingProducer( + stack_name=None, + region=None, + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + mapper=DataToJsonMapper(), + consumer=StringConsumerJsonOutput(), + ) + resource_producer.produce() + expected_output = [ + call( + '[\n {\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "HelloWorldFunctionRole",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "HelloWorldFunctionHelloWorldPermissionProd",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApiDeploymentf5716dc08b",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApiProdStage",\n "PhysicalResourceId": "-"\n }\n]' + ) + ] + self.assertEqual(expected_output, patched_click_echo.call_args_list) + + @patch("samcli.lib.translate.sam_template_validator.Session") + @patch("samcli.lib.translate.sam_template_validator.Translator") + @patch("samcli.lib.translate.sam_template_validator.parser") + def test_get_translated_template_if_valid_raises_exception(self, sam_parser, sam_translator, boto_session_patch): + managed_policy_mock = Mock() + managed_policy_mock.load.return_value = {"policy": "SomePolicy"} + template = {"a": "b"} + + parser = Mock() + sam_parser.Parser.return_value = parser + + boto_session_mock = Mock() + boto_session_patch.return_value = boto_session_mock + + translate_mock = Mock() + translate_mock.translate.side_effect = InvalidDocumentException([ExceptionWithMessage("message")]) + sam_translator.return_value = translate_mock + + validator = SamTemplateValidator(template, managed_policy_mock) + + with self.assertRaises(InvalidSamDocumentException): + validator.get_translated_template_if_valid() + + sam_translator.assert_called_once_with( + managed_policy_map={"policy": "SomePolicy"}, sam_parser=parser, plugins=[], boto_session=boto_session_mock + ) + + boto_session_patch.assert_called_once_with(profile_name=None, region_name=None) + translate_mock.translate.assert_called_once_with(sam_template=template, parameter_values={}) + sam_parser.Parser.assert_called_once() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + @patch("samcli.lib.list.resources.resource_mapping_producer.SamLocalStackProvider.get_stacks") + def test_resources_get_stacks_returns_empty( + self, + mock_get_stacks, + mock_get_translated_dict, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_get_translated_dict.return_value = {} + mock_sam_file_reader.return_value = {} + mock_get_stacks.return_value = ([], []) + with self.assertRaises(SamListLocalResourcesNotFoundError): + resource_producer = ResourceMappingProducer( + stack_name=None, + region=None, + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + mapper=DataToJsonMapper(), + consumer=StringConsumerJsonOutput(), + ) + resource_producer.produce() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_resources_info") + def test_resources_success_with_stack_name( + self, + mock_get_resources_info, + mock_get_translated_dict, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_get_resources_info.return_value = { + "StackResources": [ + {"LogicalResourceId": "HelloWorldFunction", "PhysicalResourceId": "physical_resource_1"}, + {"LogicalResourceId": "HelloWorldFunctionRole", "PhysicalResourceId": "physical_resource_2"}, + { + "LogicalResourceId": "HelloWorldFunctionHelloWorldPermissionProd", + "PhysicalResourceId": "physical_resource_3", + }, + ] + } + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + resource_producer = ResourceMappingProducer( + stack_name="test-stack", + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + mapper=DataToJsonMapper(), + consumer=StringConsumerJsonOutput(), + ) + resource_producer.produce() + expected_output = [ + call( + '[\n {\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": "physical_resource_1"\n },\n {\n "LogicalResourceId": "HelloWorldFunctionRole",\n "PhysicalResourceId": "physical_resource_2"\n },\n {\n "LogicalResourceId": "HelloWorldFunctionHelloWorldPermissionProd",\n "PhysicalResourceId": "physical_resource_3"\n },\n {\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApiDeploymentf5716dc08b",\n "PhysicalResourceId": "-"\n },\n {\n "LogicalResourceId": "ServerlessRestApiProdStage",\n "PhysicalResourceId": "-"\n }\n]' + ) + ] + self.assertEqual(expected_output, patched_click_echo.call_args_list) + + +class TestGetTranslatedDict(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.SamTemplateValidator.get_translated_template_if_valid") + def test_get_translate_dict_invalid_template_error( + self, + mock_get_translated_template_if_valid, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_sam_file_reader.return_value = { + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "sam-app-hello\nSample SAM Template for sam-app-hello\n", + "Globals": {"Function": {"Tracing": "Active", "Timeout": 3}}, + "Resources": { + "HelloWorldFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "CodeUri": "hello_world/", + "Handler": "app.lambda_handler", + "Architectures": ["x86_64"], + "Runtime": "python3.8", + "Events": {"HelloWorld": {"Type": "Api", "Properties": {"Path": "/hello", "Method": "get"}}}, + }, + } + }, + } + mock_get_translated_template_if_valid.side_effect = InvalidSamDocumentException() + with self.assertRaises(InvalidSamTemplateException): + resource_producer = ResourceMappingProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + mapper=None, + consumer=None, + ) + resource_producer.get_translated_dict(mock_sam_file_reader.return_value) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.SamTemplateValidator.get_translated_template_if_valid") + def test_get_translated_dict_clienterror_exception( + self, + mock_get_translated_template_if_valid, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_get_translated_template_if_valid.side_effect = ClientError( + {"Error": {"Code": "ExpiredToken", "Message": "The security token included in the request is expired"}}, + "DescribeStacks", + ) + with self.assertRaises(SamListUnknownClientError): + resource_producer = ResourceMappingProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + mapper=None, + consumer=None, + ) + resource_producer.get_translated_dict(mock_sam_file_reader.return_value) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.SamTemplateValidator.get_translated_template_if_valid") + def test_get_translated_dict_no_credentials_exception( + self, + mock_get_translated_template_if_valid, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_get_translated_template_if_valid.side_effect = NoCredentialsError() + with self.assertRaises(UserException): + resource_producer = ResourceMappingProducer( + stack_name=None, + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + mapper=None, + consumer=None, + ) + resource_producer.get_translated_dict(mock_sam_file_reader.return_value) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.SamTemplateValidator.get_translated_template_if_valid") + def test_get_translated_dict_no_region_found_exception( + self, + mock_get_translated_template_if_valid, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_get_translated_template_if_valid.side_effect = NoRegionFound() + with self.assertRaises(UserException): + resource_producer = ResourceMappingProducer( + stack_name=None, + region=None, + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + mapper=None, + consumer=None, + ) + resource_producer.get_translated_dict(mock_sam_file_reader.return_value) + + +class TestResourcesInitClients(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("boto3.Session.region_name", "us-east-1") + def test_init_clients_no_input_region_get_region_from_session( + self, patched_click_get_current_context, patched_click_echo + ): + with ResourcesContext( + stack_name="test", output="json", region=None, profile=None, template_file=None + ) as resources_context: + resources_context.init_clients() + self.assertEqual(resources_context.region, "us-east-1") + + +class TestGetResourcesInfo(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_clienterror_stack_does_not_exist_in_region( + self, + mock_client_provider, + mock_get_translated_dict, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.describe_stack_resources.side_effect = ClientError( + {"Error": {"Code": "ValidationError", "Message": "Stack with id test does not exist"}}, "DescribeStacks" + ) + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + with self.assertRaises(StackDoesNotExistInRegionError): + resource_producer = ResourceMappingProducer( + stack_name="test-stack", + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=mock_client_provider.return_value.return_value, + iam_client=None, + mapper=None, + consumer=None, + ) + resource_producer.get_resources_info() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_botocoreerror_invalid_region( + self, + mock_client_provider, + mock_get_translated_dict, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.describe_stack_resources.side_effect = EndpointConnectionError( + endpoint_url="https://cloudformation.test.amazonaws.com/" + ) + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + with self.assertRaises(SamListUnknownBotoCoreError): + resource_producer = ResourceMappingProducer( + stack_name="test-stack", + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=mock_client_provider.return_value.return_value, + iam_client=None, + mapper=None, + consumer=None, + ) + resource_producer.get_resources_info() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_clienterror_token_error( + self, + mock_client_provider, + mock_get_translated_dict, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.describe_stack_resources.side_effect = ClientError( + {"Error": {"Code": "ExpiredToken", "Message": "The security token included in the request is expired"}}, + "DescribeStacks", + ) + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + with self.assertRaises(SamListUnknownClientError): + resource_producer = ResourceMappingProducer( + stack_name="test-stack", + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=mock_client_provider.return_value.return_value, + iam_client=None, + mapper=None, + consumer=None, + ) + resource_producer.get_resources_info() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_stack_resource_not_in_response( + self, + mock_client_provider, + mock_get_translated_dict, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.describe_stack_resources.return_value = {} + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + resource_producer = ResourceMappingProducer( + stack_name="test-stack", + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=mock_client_provider.return_value.return_value, + iam_client=None, + mapper=None, + consumer=None, + ) + response = resource_producer.get_resources_info() + self.assertEqual(response, {"StackResources": []}) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + @patch("samcli.lib.list.resources.resource_mapping_producer.ResourceMappingProducer.get_translated_dict") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_stack_resource_in_response( + self, + mock_client_provider, + mock_get_translated_dict, + mock_sam_file_reader, + patched_click_get_current_context, + patched_click_echo, + ): + mock_client_provider.return_value.return_value.describe_stack_resources.return_value = { + "StackResources": [{"StackName": "sam-app-hello"}] + } + mock_get_translated_dict.return_value = TRANSLATED_DICT_RETURN + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + resource_producer = ResourceMappingProducer( + stack_name="test-stack", + region="us-east-1", + profile=None, + template_file=None, + cloudformation_client=mock_client_provider.return_value.return_value, + iam_client=None, + mapper=None, + consumer=None, + ) + response = resource_producer.get_resources_info() + self.assertEqual(response, {"StackResources": [{"StackName": "sam-app-hello"}]}) diff --git a/tests/unit/commands/list/stack_outputs/__init__.py b/tests/unit/commands/list/stack_outputs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/commands/list/stack_outputs/test_cli.py b/tests/unit/commands/list/stack_outputs/test_cli.py new file mode 100644 index 0000000000..35ea203f74 --- /dev/null +++ b/tests/unit/commands/list/stack_outputs/test_cli.py @@ -0,0 +1,33 @@ +from unittest import TestCase +from unittest.mock import Mock, patch +from samcli.commands.list.stack_outputs.command import do_cli + + +class TestCli(TestCase): + def setUp(self): + self.stack_name = "stack-name" + self.output = "json" + self.region = None + self.profile = None + + @patch("samcli.commands.list.stack_outputs.command.click") + @patch("samcli.commands.list.stack_outputs.stack_outputs_context.StackOutputsContext") + def test_cli_base_command(self, mock_stack_outputs_context, mock_stack_outputs_click): + context_mock = Mock() + mock_stack_outputs_context.return_value.__enter__.return_value = context_mock + do_cli( + stack_name=self.stack_name, + output=self.output, + region=self.region, + profile=self.profile, + ) + + mock_stack_outputs_context.assert_called_with( + stack_name=self.stack_name, + output=self.output, + region=self.region, + profile=self.profile, + ) + + context_mock.run.assert_called_with() + self.assertEqual(context_mock.run.call_count, 1) diff --git a/tests/unit/commands/list/stack_outputs/test_stack_outputs_context.py b/tests/unit/commands/list/stack_outputs/test_stack_outputs_context.py new file mode 100644 index 0000000000..fa844c21fc --- /dev/null +++ b/tests/unit/commands/list/stack_outputs/test_stack_outputs_context.py @@ -0,0 +1,101 @@ +from unittest import TestCase +from unittest.mock import patch, call +from botocore.exceptions import ClientError, EndpointConnectionError + +from samcli.commands.list.stack_outputs.stack_outputs_context import StackOutputsContext +from samcli.commands.exceptions import RegionError +from samcli.commands.list.exceptions import SamListError, NoOutputsForStackError, StackDoesNotExistInRegionError + + +class TestStackOutputsContext(TestCase): + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_stack_outputs_stack_exists( + self, mock_client_provider, patched_click_get_current_context, patched_click_echo + ): + mock_client_provider.return_value.return_value.describe_stacks.return_value = { + "Stacks": [{"Outputs": [{"OutputKey": "HelloWorldTest", "OutputValue": "TestVal", "Description": "Test"}]}] + } + with StackOutputsContext( + stack_name="test", output="json", region="us-east-1", profile=None + ) as stack_output_context: + + stack_output_context.run() + expected_click_echo_calls = [ + call( + '[\n {\n "OutputKey": "HelloWorldTest",\n "OutputValue": "TestVal",\n "Description": "Test"\n }\n]' + ) + ] + self.assertEqual( + expected_click_echo_calls, patched_click_echo.call_args_list, "Stack and stack outputs should exist" + ) + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_no_stack_object_in_response( + self, mock_client_provider, patched_click_get_current_context, patched_click_echo + ): + mock_client_provider.return_value.return_value.describe_stacks.return_value = {"Stacks": []} + with self.assertRaises(StackDoesNotExistInRegionError): + with StackOutputsContext( + stack_name="test", output="json", region="us-east-1", profile=None + ) as stack_output_context: + stack_output_context.run() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_no_output_object_in_response( + self, mock_client_provider, patched_click_get_current_context, patched_click_echo + ): + mock_client_provider.return_value.return_value.describe_stacks.return_value = {"Stacks": [{}]} + with self.assertRaises(NoOutputsForStackError): + with StackOutputsContext( + stack_name="test", output="json", region="us-east-1", profile=None + ) as stack_output_context: + stack_output_context.run() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_clienterror_stack_does_not_exist_in_region( + self, mock_client_provider, patched_click_get_current_context, patched_click_echo + ): + mock_client_provider.return_value.return_value.describe_stacks.side_effect = ClientError( + {"Error": {"Code": "ValidationError", "Message": "Stack with id test does not exist"}}, "DescribeStacks" + ) + with self.assertRaises(StackDoesNotExistInRegionError): + with StackOutputsContext( + stack_name="test", output="json", region="us-east-1", profile=None + ) as stack_output_context: + stack_output_context.run() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.commands.list.cli_common.list_common_context.get_boto_client_provider_with_config") + def test_botocoreerror_invalid_region( + self, mock_client_provider, patched_click_get_current_context, patched_click_echo + ): + mock_client_provider.return_value.return_value.describe_stacks.side_effect = EndpointConnectionError( + endpoint_url="https://cloudformation.test.amazonaws.com/" + ) + with self.assertRaises(SamListError): + with StackOutputsContext( + stack_name="test", output="json", region="us-east-1", profile=None + ) as stack_output_context: + stack_output_context.run() + + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("boto3.Session.region_name", "us-east-1") + def test_init_clients_has_region(self, patched_click_get_current_context, patched_click_echo): + with StackOutputsContext( + stack_name="test", + output="json", + region=None, + profile=None, + ) as stack_output_context: + stack_output_context.init_clients() + self.assertEqual(stack_output_context.region, "us-east-1") diff --git a/tests/unit/commands/list/test_list_mappers.py b/tests/unit/commands/list/test_list_mappers.py new file mode 100644 index 0000000000..b40a1b1ae2 --- /dev/null +++ b/tests/unit/commands/list/test_list_mappers.py @@ -0,0 +1,105 @@ +from unittest import TestCase +from unittest.mock import patch, call +from collections import OrderedDict +from samcli.lib.list.resources.resources_to_table_mapper import ResourcesToTableMapper +from samcli.lib.list.stack_outputs.stack_output_to_table_mapper import StackOutputToTableMapper +from samcli.lib.list.data_to_json_mapper import DataToJsonMapper +from samcli.commands.list.json_consumer import StringConsumerJsonOutput +from samcli.lib.list.endpoints.endpoints_to_table_mapper import EndpointsToTableMapper +from samcli.lib.list.mapper_consumer_factory import MapperConsumerFactory +from samcli.lib.list.list_interfaces import ProducersEnum +from samcli.commands.list.table_consumer import StringConsumerTableOutput + + +class TestStackOutputsToTableMapper(TestCase): + def test_map(self): + data = [{"OutputKey": "outputkey1", "OutputValue": "outputvalue1", "Description": "sample description"}] + stack_outputs_to_table_mapper = StackOutputToTableMapper() + output = stack_outputs_to_table_mapper.map(data) + self.assertEqual(output.get("table_name", ""), "Stack Outputs") + + +class TestResourcesToTableMapper(TestCase): + def test_map(self): + data = [{"LogicalResourceId": "LID_1", "PhysicalResourceId": "PID_1"}] + resources_to_table_mapper = ResourcesToTableMapper() + output = resources_to_table_mapper.map(data) + self.assertEqual(output.get("table_name", ""), "Resources") + + +class TestEndpointsToTableMapper(TestCase): + def test_map(self): + data = [ + { + "LogicalResourceId": "LID_1", + "PhysicalResourceId": "PID_1", + "CloudEndpoint": "test.url", + "Methods": "-", + }, + { + "LogicalResourceId": "LID_1", + "PhysicalResourceId": "PID_1", + "CloudEndpoint": "-", + "Methods": "-", + }, + { + "LogicalResourceId": "LID_1", + "PhysicalResourceId": "PID_1", + "CloudEndpoint": ["api.url1"], + "Methods": "-", + }, + { + "LogicalResourceId": "LID_1", + "PhysicalResourceId": "PID_1", + "CloudEndpoint": ["api.url1", "api.url2", "api.url3"], + "Methods": ["/hello2['get, put']", "/hello['get']"], + }, + ] + endpoints_to_table_mapper = EndpointsToTableMapper() + output = endpoints_to_table_mapper.map(data) + self.assertEqual(output.get("table_name", ""), "Endpoints") + + +class TestMapperConsumerFactory(TestCase): + def test_create_json_output(self): + factory = MapperConsumerFactory() + container = factory.create(ProducersEnum.STACK_OUTPUTS_PRODUCER, "json") + self.assertIsInstance(container.mapper, DataToJsonMapper) + self.assertIsInstance(container.consumer, StringConsumerJsonOutput) + + def test_create_stack_outputs_table_output(self): + factory = MapperConsumerFactory() + container = factory.create(ProducersEnum.STACK_OUTPUTS_PRODUCER, "table") + self.assertIsInstance(container.mapper, StackOutputToTableMapper) + self.assertIsInstance(container.consumer, StringConsumerTableOutput) + + def test_create_resources_table_output(self): + factory = MapperConsumerFactory() + container = factory.create(ProducersEnum.RESOURCES_PRODUCER, "table") + self.assertIsInstance(container.mapper, ResourcesToTableMapper) + self.assertIsInstance(container.consumer, StringConsumerTableOutput) + + def test_create_endpoints_table_output(self): + factory = MapperConsumerFactory() + container = factory.create(ProducersEnum.ENDPOINTS_PRODUCER, "table") + self.assertIsInstance(container.mapper, EndpointsToTableMapper) + self.assertIsInstance(container.consumer, StringConsumerTableOutput) + + +class TestTableConsumer(TestCase): + @patch("samcli.commands.list.json_consumer.click.secho") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + def test_consume(self, patched_click_get_current_context, patched_click_echo): + consumer = StringConsumerTableOutput() + data = { + "format_string": "{OutputKey:<{0}} {OutputValue:<{1}} {Description:<{2}}", + "format_args": OrderedDict( + {"OutputKey": "OutputKey", "OutputValue": "OutputValue", "Description": "Description"} + ), + "table_name": "Stack Outputs", + "data": [], + } + consumer.consume(data) + print(patched_click_echo.call_args_list) + self.assertTrue(patched_click_echo.call_args_list) + self.assertEqual(call("Stack Outputs"), patched_click_echo.call_args_list[0]) diff --git a/tests/unit/commands/validate/lib/test_sam_template_validator.py b/tests/unit/commands/validate/lib/test_sam_template_validator.py index 5f4e48729c..a12794d8f0 100644 --- a/tests/unit/commands/validate/lib/test_sam_template_validator.py +++ b/tests/unit/commands/validate/lib/test_sam_template_validator.py @@ -7,13 +7,13 @@ from samtranslator.public.exceptions import InvalidDocumentException from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException -from samcli.commands.validate.lib.sam_template_validator import SamTemplateValidator +from samcli.lib.translate.sam_template_validator import SamTemplateValidator class TestSamTemplateValidator(TestCase): - @patch("samcli.commands.validate.lib.sam_template_validator.Session") - @patch("samcli.commands.validate.lib.sam_template_validator.Translator") - @patch("samcli.commands.validate.lib.sam_template_validator.parser") + @patch("samcli.lib.translate.sam_template_validator.Session") + @patch("samcli.lib.translate.sam_template_validator.Translator") + @patch("samcli.lib.translate.sam_template_validator.parser") def test_is_valid_returns_true(self, sam_parser, sam_translator, boto_session_patch): managed_policy_mock = Mock() managed_policy_mock.load.return_value = {"policy": "SomePolicy"} @@ -32,7 +32,7 @@ def test_is_valid_returns_true(self, sam_parser, sam_translator, boto_session_pa validator = SamTemplateValidator(template, managed_policy_mock, profile="profile", region="region") # Should not throw an Exception - validator.is_valid() + validator.get_translated_template_if_valid() boto_session_patch.assert_called_once_with(profile_name="profile", region_name="region") sam_translator.assert_called_once_with( @@ -41,9 +41,9 @@ def test_is_valid_returns_true(self, sam_parser, sam_translator, boto_session_pa translate_mock.translate.assert_called_once_with(sam_template=template, parameter_values={}) sam_parser.Parser.assert_called_once() - @patch("samcli.commands.validate.lib.sam_template_validator.Session") - @patch("samcli.commands.validate.lib.sam_template_validator.Translator") - @patch("samcli.commands.validate.lib.sam_template_validator.parser") + @patch("samcli.lib.translate.sam_template_validator.Session") + @patch("samcli.lib.translate.sam_template_validator.Translator") + @patch("samcli.lib.translate.sam_template_validator.parser") def test_is_valid_raises_exception(self, sam_parser, sam_translator, boto_session_patch): managed_policy_mock = Mock() managed_policy_mock.load.return_value = {"policy": "SomePolicy"} @@ -64,7 +64,7 @@ def test_is_valid_raises_exception(self, sam_parser, sam_translator, boto_sessio validator = SamTemplateValidator(template, managed_policy_mock) with self.assertRaises(InvalidSamDocumentException): - validator.is_valid() + validator.get_translated_template_if_valid() sam_translator.assert_called_once_with( managed_policy_map={"policy": "SomePolicy"}, sam_parser=parser, plugins=[], boto_session=boto_session_mock diff --git a/tests/unit/commands/validate/test_cli.py b/tests/unit/commands/validate/test_cli.py index 16d46a8e73..11868d1e2f 100644 --- a/tests/unit/commands/validate/test_cli.py +++ b/tests/unit/commands/validate/test_cli.py @@ -40,7 +40,7 @@ def test_file_parsed(self, path_exists_patch, click_patch, yaml_parse_patch): self.assertEqual(actual_template, {"a": "b"}) - @patch("samcli.commands.validate.lib.sam_template_validator.SamTemplateValidator") + @patch("samcli.lib.translate.sam_template_validator.SamTemplateValidator") @patch("samcli.commands.validate.validate.click") @patch("samcli.commands.validate.validate._read_sam_file") @patch("boto3.client") @@ -48,14 +48,14 @@ def test_template_fails_validation(self, patched_boto, read_sam_file_patch, clic template_path = "path_to_template" read_sam_file_patch.return_value = {"a": "b"} - is_valid_mock = Mock() - is_valid_mock.is_valid.side_effect = InvalidSamDocumentException - template_valiadator.return_value = is_valid_mock + get_translated_template_if_valid_mock = Mock() + get_translated_template_if_valid_mock.get_translated_template_if_valid.side_effect = InvalidSamDocumentException + template_valiadator.return_value = get_translated_template_if_valid_mock with self.assertRaises(InvalidSamTemplateException): do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) - @patch("samcli.commands.validate.lib.sam_template_validator.SamTemplateValidator") + @patch("samcli.lib.translate.sam_template_validator.SamTemplateValidator") @patch("samcli.commands.validate.validate.click") @patch("samcli.commands.validate.validate._read_sam_file") @patch("boto3.client") @@ -63,14 +63,14 @@ def test_no_credentials_provided(self, patched_boto, read_sam_file_patch, click_ template_path = "path_to_template" read_sam_file_patch.return_value = {"a": "b"} - is_valid_mock = Mock() - is_valid_mock.is_valid.side_effect = NoCredentialsError - template_valiadator.return_value = is_valid_mock + get_translated_template_if_valid_mock = Mock() + get_translated_template_if_valid_mock.get_translated_template_if_valid.side_effect = NoCredentialsError + template_valiadator.return_value = get_translated_template_if_valid_mock with self.assertRaises(UserException): do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) - @patch("samcli.commands.validate.lib.sam_template_validator.SamTemplateValidator") + @patch("samcli.lib.translate.sam_template_validator.SamTemplateValidator") @patch("samcli.commands.validate.validate.click") @patch("samcli.commands.validate.validate._read_sam_file") @patch("boto3.client") @@ -78,9 +78,9 @@ def test_template_passes_validation(self, patched_boto, read_sam_file_patch, cli template_path = "path_to_template" read_sam_file_patch.return_value = {"a": "b"} - is_valid_mock = Mock() - is_valid_mock.is_valid.return_value = True - template_valiadator.return_value = is_valid_mock + get_translated_template_if_valid_mock = Mock() + get_translated_template_if_valid_mock.get_translated_template_if_valid.return_value = True + template_valiadator.return_value = get_translated_template_if_valid_mock do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) diff --git a/tests/unit/lib/iac/cfn/test_cfn_iac_implementation.py b/tests/unit/lib/iac/cfn/test_cfn_iac_implementation.py index fc5aaa625a..995bc31884 100644 --- a/tests/unit/lib/iac/cfn/test_cfn_iac_implementation.py +++ b/tests/unit/lib/iac/cfn/test_cfn_iac_implementation.py @@ -1,7 +1,6 @@ -import copy import os from unittest import TestCase -from unittest.mock import patch, Mock, ANY +from unittest.mock import patch, Mock from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException from samcli.lib.iac.cfn.cfn_iac import CfnIacImplementation From 7ae6b7f89e6e232959972dd5293a91c65e8e4de5 Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Fri, 27 Jan 2023 09:54:13 -0800 Subject: [PATCH 04/11] feat: Add warning about not providing stack name option (#4624) --- samcli/commands/list/cli_common/options.py | 8 ++++++++ samcli/commands/list/endpoints/command.py | 4 +++- samcli/commands/list/resources/command.py | 4 +++- .../unit/commands/list/endpoints/test_cli.py | 19 ++++++++++++++++++- .../unit/commands/list/resources/test_cli.py | 19 ++++++++++++++++++- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/samcli/commands/list/cli_common/options.py b/samcli/commands/list/cli_common/options.py index b214ec9da7..2756542594 100644 --- a/samcli/commands/list/cli_common/options.py +++ b/samcli/commands/list/cli_common/options.py @@ -32,3 +32,11 @@ def output_click_option(): def output_option(f): return output_click_option()(f) + + +def stack_name_not_provided_message(): + click.secho( + fg="yellow", + message="The --stack-name options was not provided, displaying only local template data. " + "To see data about deployed resources, provide the corresponding stack name.", + ) diff --git a/samcli/commands/list/endpoints/command.py b/samcli/commands/list/endpoints/command.py index 7a783c3789..024b6a0f59 100644 --- a/samcli/commands/list/endpoints/command.py +++ b/samcli/commands/list/endpoints/command.py @@ -5,7 +5,7 @@ import click from samcli.commands._utils.command_exception_handler import command_exception_handler -from samcli.commands.list.cli_common.options import stack_name_option, output_option +from samcli.commands.list.cli_common.options import stack_name_option, output_option, stack_name_not_provided_message from samcli.cli.main import pass_context, common_options, aws_creds_options, print_cmdline_args from samcli.lib.utils.version_checker import check_newer_version from samcli.lib.telemetry.metric import track_command @@ -49,4 +49,6 @@ def do_cli(stack_name, output, region, profile, template_file): with EndpointsContext( stack_name=stack_name, output=output, region=region, profile=profile, template_file=template_file ) as endpoints_context: + if not stack_name: + stack_name_not_provided_message() endpoints_context.run() diff --git a/samcli/commands/list/resources/command.py b/samcli/commands/list/resources/command.py index 7e00460090..19d99b3d3e 100644 --- a/samcli/commands/list/resources/command.py +++ b/samcli/commands/list/resources/command.py @@ -5,7 +5,7 @@ import click from samcli.commands._utils.command_exception_handler import command_exception_handler -from samcli.commands.list.cli_common.options import stack_name_option, output_option +from samcli.commands.list.cli_common.options import stack_name_option, output_option, stack_name_not_provided_message from samcli.cli.main import pass_context, common_options, aws_creds_options, print_cmdline_args from samcli.lib.utils.version_checker import check_newer_version from samcli.lib.telemetry.metric import track_command @@ -49,4 +49,6 @@ def do_cli(stack_name, output, region, profile, template_file): with ResourcesContext( stack_name=stack_name, output=output, region=region, profile=profile, template_file=template_file ) as resources_context: + if not stack_name: + stack_name_not_provided_message() resources_context.run() diff --git a/tests/unit/commands/list/endpoints/test_cli.py b/tests/unit/commands/list/endpoints/test_cli.py index 3ff7c8ac4b..40b5d9b2fc 100644 --- a/tests/unit/commands/list/endpoints/test_cli.py +++ b/tests/unit/commands/list/endpoints/test_cli.py @@ -11,9 +11,10 @@ def setUp(self): self.profile = None self.template_file = None + @patch("samcli.commands.list.endpoints.command.stack_name_not_provided_message") @patch("samcli.commands.list.endpoints.command.click") @patch("samcli.commands.list.endpoints.endpoints_context.EndpointsContext") - def test_cli_base_command(self, mock_endpoints_context, mock_endpoints_click): + def test_cli_base_command(self, mock_endpoints_context, mock_endpoints_click, mock_stack_name_not_provided): context_mock = Mock() mock_endpoints_context.return_value.__enter__.return_value = context_mock do_cli( @@ -34,3 +35,19 @@ def test_cli_base_command(self, mock_endpoints_context, mock_endpoints_click): context_mock.run.assert_called_with() self.assertEqual(context_mock.run.call_count, 1) + mock_stack_name_not_provided.assert_not_called() + + @patch("samcli.commands.list.endpoints.command.stack_name_not_provided_message") + @patch("samcli.commands.list.endpoints.command.click") + @patch("samcli.commands.list.endpoints.endpoints_context.EndpointsContext") + def test_warns_user_stack_name_not_provided( + self, mock_resources_context, mock_resources_click, mock_stack_name_not_provided + ): + do_cli( + stack_name=None, + output=self.output, + region=self.region, + profile=self.profile, + template_file=self.template_file, + ) + mock_stack_name_not_provided.assert_called_once() diff --git a/tests/unit/commands/list/resources/test_cli.py b/tests/unit/commands/list/resources/test_cli.py index 57dea557d3..d5ca3ef7af 100644 --- a/tests/unit/commands/list/resources/test_cli.py +++ b/tests/unit/commands/list/resources/test_cli.py @@ -11,9 +11,10 @@ def setUp(self): self.profile = None self.template_file = None + @patch("samcli.commands.list.resources.command.stack_name_not_provided_message") @patch("samcli.commands.list.resources.command.click") @patch("samcli.commands.list.resources.resources_context.ResourcesContext") - def test_cli_base_command(self, mock_resources_context, mock_resources_click): + def test_cli_base_command(self, mock_resources_context, mock_resources_click, mock_stack_name_not_provided): context_mock = Mock() mock_resources_context.return_value.__enter__.return_value = context_mock do_cli( @@ -34,3 +35,19 @@ def test_cli_base_command(self, mock_resources_context, mock_resources_click): context_mock.run.assert_called_with() self.assertEqual(context_mock.run.call_count, 1) + mock_stack_name_not_provided.assert_not_called() + + @patch("samcli.commands.list.resources.command.stack_name_not_provided_message") + @patch("samcli.commands.list.resources.command.click") + @patch("samcli.commands.list.resources.resources_context.ResourcesContext") + def test_warns_user_stack_name_not_provided( + self, mock_resources_context, mock_resources_click, mock_stack_name_not_provided + ): + do_cli( + stack_name=None, + output=self.output, + region=self.region, + profile=self.profile, + template_file=self.template_file, + ) + mock_stack_name_not_provided.assert_called_once() From c2573230bcf20df24c23c03d72b018587b1b3f08 Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Fri, 27 Jan 2023 11:28:13 -0800 Subject: [PATCH 05/11] fix: Fix failing list tests on Windows (#4623) * fix: Fix failing list tests on Windows * Black reformat --- .../list/endpoints/endpoints_integ_base.py | 16 ++++ .../list/endpoints/test_endpoints_command.py | 81 ++++++------------- tests/integration/list/list_integ_base.py | 10 +++ .../list/resources/resources_integ_base.py | 6 ++ .../list/resources/test_resources_command.py | 55 +++++-------- 5 files changed, 78 insertions(+), 90 deletions(-) diff --git a/tests/integration/list/endpoints/endpoints_integ_base.py b/tests/integration/list/endpoints/endpoints_integ_base.py index a5dc3087af..6740b8ab26 100644 --- a/tests/integration/list/endpoints/endpoints_integ_base.py +++ b/tests/integration/list/endpoints/endpoints_integ_base.py @@ -25,3 +25,19 @@ def get_endpoints_command_list( command_list += ["--help"] return command_list + + def assert_endpoints(self, endpoints, logical_id, physical_id, cloud_endpoints, methods): + resource = self._find_resource(endpoints, logical_id) + if not resource: + raise AssertionError(f"Couldn't find endpoint with corresponding logical id {logical_id}") + self.assertRegex(resource.get("PhysicalResourceId", ""), physical_id) + self.assertEqual(resource.get("Methods", []), methods) + self._assert_cloud_endpoints(resource, cloud_endpoints) + + def _assert_cloud_endpoints(self, resource, cloud_endpoints): + deployed_endpoint = resource.get("CloudEndpoint") + if isinstance(cloud_endpoints, str): + self.assertRegex(deployed_endpoint, cloud_endpoints) + return + for deployed, expected in zip(deployed_endpoint, cloud_endpoints): + self.assertRegex(deployed, expected) diff --git a/tests/integration/list/endpoints/test_endpoints_command.py b/tests/integration/list/endpoints/test_endpoints_command.py index 5fc96b56a5..a3aea8840e 100644 --- a/tests/integration/list/endpoints/test_endpoints_command.py +++ b/tests/integration/list/endpoints/test_endpoints_command.py @@ -1,7 +1,7 @@ import os import time import boto3 -import re +import json from unittest import skipIf from tests.integration.deploy.deploy_integ_base import DeployIntegBase from tests.integration.list.endpoints.endpoints_integ_base import EndpointsIntegBase @@ -42,31 +42,17 @@ def test_no_stack_name(self): stack_name=None, output="json", region=region, template_file=template_path ) command_result = run_command(cmdlist, cwd=self.working_dir) - expected_output = [ - """{ - "LogicalResourceId": "HelloWorldFunction", - "PhysicalResourceId": "-", - "CloudEndpoint": "-", - "Methods": "-" - }""", - """{ - "LogicalResourceId": "TestAPI", - "PhysicalResourceId": "-", - "CloudEndpoint": "-", - "Methods": [] - }""", - """{ - "LogicalResourceId": "ServerlessRestApi", - "PhysicalResourceId": "-", - "CloudEndpoint": "-", - "Methods": [ - "/hello2['get']", - "/hello['get']" - ] - }""", - ] - for expression in expected_output: - self.assertIn(expression, command_result.stdout.decode()) + command_output = json.loads(command_result.stdout.decode()) + self.assertEqual(len(command_output), 3) + self.assert_endpoints(command_output, "HelloWorldFunction", "-", "-", "-") + self.assert_endpoints( + command_output, + "ServerlessRestApi", + "-", + [], + ["/hello2['get']", "/hello['get']"], + ) + self.assert_endpoints(command_output, "TestAPI", "-", "-", []) def test_has_stack_name(self): template_path = self.list_test_data_path.joinpath("test_endpoints_template.yaml") @@ -86,36 +72,19 @@ def test_has_stack_name(self): stack_name=stack_name, output="json", region=region, template_file=template_path ) command_result = run_command(cmdlist, cwd=self.working_dir) - expected_output = [ - """{ - "LogicalResourceId": "HelloWorldFunction", - "PhysicalResourceId": "test-has-stack-name.*", - "CloudEndpoint": "https://.*.lambda-url..*.on.aws/", - "Methods": "-" - }""", - """ { - "LogicalResourceId": "ServerlessRestApi", - "PhysicalResourceId": ".*", - "CloudEndpoint": .* - "https://.*.execute-api..*.amazonaws.com/Prod", - "https://.*.execute-api..*.amazonaws.com/Stage" - .*, - "Methods": .* - "/hello2.'get'.", - "/hello.'get'." - . - }""", - """ { - "LogicalResourceId": "TestAPI", - "PhysicalResourceId": ".*", - "CloudEndpoint": . - "https://.*.execute-api..*.amazonaws.com/Test2" - ., - "Methods": .. - }""", - ] - for expression in expected_output: - self.assertTrue(re.search(expression, command_result.stdout.decode())) + command_output = json.loads(command_result.stdout.decode()) + self.assertEqual(len(command_output), 3) + self.assert_endpoints( + command_output, "HelloWorldFunction", "test-has-stack-name.*", "https://.*.lambda-url..*.on.aws/", "-" + ) + self.assert_endpoints( + command_output, + "ServerlessRestApi", + ".*", + ["https://.*.execute-api..*.amazonaws.com/Prod", "https://.*.execute-api..*.amazonaws.com/Stage"], + ["/hello2['get']", "/hello['get']"], + ) + self.assert_endpoints(command_output, "TestAPI", ".*", ["https://.*.execute-api..*.amazonaws.com/Test2"], []) def test_stack_does_not_exist(self): template_path = self.list_test_data_path.joinpath("test_endpoints_template.yaml") diff --git a/tests/integration/list/list_integ_base.py b/tests/integration/list/list_integ_base.py index e5055e4797..562a301215 100644 --- a/tests/integration/list/list_integ_base.py +++ b/tests/integration/list/list_integ_base.py @@ -1,3 +1,5 @@ +import re + import os from unittest import TestCase from pathlib import Path @@ -34,3 +36,11 @@ def cleanup_config(self): @classmethod def base_command(cls): return get_sam_command() + + @staticmethod + def _find_resource(resources, logical_id): + for resource in resources: + resource_logical_id = resource.get("LogicalResourceId", "") + if resource_logical_id == logical_id or re.match(logical_id, resource_logical_id): + return resource + return None diff --git a/tests/integration/list/resources/resources_integ_base.py b/tests/integration/list/resources/resources_integ_base.py index deccf660e2..cdeae56854 100644 --- a/tests/integration/list/resources/resources_integ_base.py +++ b/tests/integration/list/resources/resources_integ_base.py @@ -25,3 +25,9 @@ def get_resources_command_list( command_list += ["--help"] return command_list + + def assert_resource(self, resources, logical_id, physical_id): + resource = self._find_resource(resources, logical_id) + if not resource: + raise AssertionError(f"Couldn't find resource with corresponding logical id {logical_id}") + self.assertRegex(resource.get("PhysicalResourceId", ""), physical_id) diff --git a/tests/integration/list/resources/test_resources_command.py b/tests/integration/list/resources/test_resources_command.py index 4f1cf8533e..1a816d50c9 100644 --- a/tests/integration/list/resources/test_resources_command.py +++ b/tests/integration/list/resources/test_resources_command.py @@ -1,7 +1,7 @@ import os import time import boto3 -import re +import json from unittest import skipIf from tests.integration.deploy.deploy_integ_base import DeployIntegBase from tests.integration.list.resources.resources_integ_base import ResourcesIntegBase @@ -42,24 +42,14 @@ def test_successful_transform(self): stack_name=None, region=region, output="json", template_file=template_path ) command_result = run_command(cmdlist, cwd=self.working_dir) - expression_list = [ - """{\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": "-"\n }""", - """{\n "LogicalResourceId": "HelloWorldFunctionRole",\n "PhysicalResourceId": "-"\n }""", - """{\n "LogicalResourceId": "HelloWorldFunctionHelloWorldPermissionProd",\n "PhysicalResourceId": "-"\n }""", - """{\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": "-"\n }""", - """{\n "LogicalResourceId": "ServerlessRestApiProdStage",\n "PhysicalResourceId": "-"\n }""", - ] - for expression in expression_list: - self.assertIn( - expression, - command_result.stdout.decode(), - ) - self.assertTrue( - re.search( - """{\n "LogicalResourceId": "ServerlessRestApiDeployment.*",\n "PhysicalResourceId": "-"\n }""", - command_result.stdout.decode(), - ) - ) + command_output = json.loads(command_result.stdout.decode()) + self.assertEqual(len(command_output), 6) + self.assert_resource(command_output, "HelloWorldFunction", "-") + self.assert_resource(command_output, "HelloWorldFunctionRole", "-") + self.assert_resource(command_output, "HelloWorldFunctionHelloWorldPermissionProd", "-") + self.assert_resource(command_output, "ServerlessRestApi", "-") + self.assert_resource(command_output, "ServerlessRestApiProdStage", "-") + self.assert_resource(command_output, "ServerlessRestApiDeployment.*", "-") def test_invalid_template_file(self): template_path = self.list_test_data_path.joinpath("test_resources_invalid_sam_template.yaml") @@ -91,21 +81,18 @@ def test_success_with_stack_name(self): stack_name=stack_name, region=region, output="json", template_file=template_path ) command_result = run_command(cmdlist, cwd=self.working_dir) - expression_list = [ - """{\n "LogicalResourceId": "HelloWorldFunction",\n "PhysicalResourceId": ".*HelloWorldFunction.*"\n }""", - """{\n "LogicalResourceId": "HelloWorldFunctionRole",\n "PhysicalResourceId": ".*HelloWorldFunctionRole.*"\n }""", - """{\n "LogicalResourceId": "HelloWorldFunctionHelloWorldPermissionProd",\n "PhysicalResourceId": ".*HelloWorldFunctionHelloWorldPermissionProd.*"\n }""", - """{\n "LogicalResourceId": "ServerlessRestApi",\n "PhysicalResourceId": ".*"\n }""", - """{\n "LogicalResourceId": "ServerlessRestApiProdStage",\n "PhysicalResourceId": ".*"\n }""", - """{\n "LogicalResourceId": "ServerlessRestApiDeployment.*",\n "PhysicalResourceId": ".*"\n }""", - ] - for expression in expression_list: - self.assertTrue( - re.search( - expression, - command_result.stdout.decode(), - ) - ) + command_output = json.loads(command_result.stdout.decode()) + self.assertEqual(len(command_output), 7) + self.assert_resource(command_output, "HelloWorldFunction", ".*HelloWorldFunction.*") + self.assert_resource(command_output, "HelloWorldFunctionRole", ".*HelloWorldFunctionRole.*") + self.assert_resource( + command_output, + "HelloWorldFunctionHelloWorldPermissionProd", + ".*HelloWorldFunctionHelloWorldPermissionProd.*", + ) + self.assert_resource(command_output, "ServerlessRestApi", ".*") + self.assert_resource(command_output, "ServerlessRestApiProdStage", ".*") + self.assert_resource(command_output, "ServerlessRestApiDeployment.*", ".*") def test_stack_does_not_exist(self): template_path = self.list_test_data_path.joinpath("test_stack_creation_template.yaml") From bf661932137237e18a9415dcc5e6f1f8cf1310df Mon Sep 17 00:00:00 2001 From: David <114027923+cdavidxu-hub@users.noreply.github.com> Date: Mon, 30 Jan 2023 01:44:40 -0800 Subject: [PATCH 06/11] Add event tracking for sam validate --lint metrics (#4612) * Update lint helpand output message * Add event tracking for sam validate --lint metrics * Add unit test for tracking --------- Co-authored-by: Sriram Madapusi Vasudevan <3770774+sriram-mv@users.noreply.github.com> Co-authored-by: Qingchuan Ma <69653965+qingchm@users.noreply.github.com> Co-authored-by: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> --- samcli/commands/validate/validate.py | 2 ++ samcli/lib/telemetry/event.py | 1 + tests/unit/commands/validate/test_cli.py | 10 +++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/samcli/commands/validate/validate.py b/samcli/commands/validate/validate.py index d7ef7d384f..f2f2f60181 100644 --- a/samcli/commands/validate/validate.py +++ b/samcli/commands/validate/validate.py @@ -12,6 +12,7 @@ from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options, print_cmdline_args from samcli.commands._utils.cdk_support_decorators import unsupported_command_cdk from samcli.commands._utils.options import template_option_without_build +from samcli.lib.telemetry.event import EventTracker from samcli.lib.telemetry.metric import track_command from samcli.cli.cli_config_file import configuration_option, TomlProvider from samcli.lib.utils.version_checker import check_newer_version @@ -130,6 +131,7 @@ def _lint(ctx: Context, template: str) -> None: cfn_lint_logger = logging.getLogger("cfnlint") cfn_lint_logger.propagate = False + EventTracker.track_event("UsedFeature", "CFNLint") try: lint_args = [template] diff --git a/samcli/lib/telemetry/event.py b/samcli/lib/telemetry/event.py index 41b96073a2..4dbfe43eb4 100644 --- a/samcli/lib/telemetry/event.py +++ b/samcli/lib/telemetry/event.py @@ -34,6 +34,7 @@ class UsedFeature(Enum): ACCELERATE = "Accelerate" CDK = "CDK" INIT_WITH_APPLICATION_INSIGHTS = "InitWithApplicationInsights" + CFNLint = "CFNLint" class EventType: diff --git a/tests/unit/commands/validate/test_cli.py b/tests/unit/commands/validate/test_cli.py index 11868d1e2f..82b6312e39 100644 --- a/tests/unit/commands/validate/test_cli.py +++ b/tests/unit/commands/validate/test_cli.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, patch from collections import namedtuple -from botocore.exceptions import NoCredentialsError, InvalidRegionError +from botocore.exceptions import NoCredentialsError from cfnlint.core import CfnLintExitException, InvalidRegionException # type: ignore @@ -118,3 +118,11 @@ def test_lint_exception_fails(self, click_patch, matches_patch, args_patch): with self.assertRaises(UserException): _lint(ctx=ctx_lint_mock(debug=False, region="region"), template=template_path) + + @patch("samcli.commands.validate.validate.click") + def test_lint_event_recorded(self, click_patch): + template_path = "path_to_template" + + with patch("samcli.lib.telemetry.event.EventTracker.track_event") as track_patch: + _lint(ctx=ctx_lint_mock(debug=False, region="region"), template=template_path) + track_patch.assert_called_with("UsedFeature", "CFNLint") From 14f751aeb04c084abd00e8f7ab2696a5c536f296 Mon Sep 17 00:00:00 2001 From: Qingchuan Ma <69653965+qingchm@users.noreply.github.com> Date: Mon, 30 Jan 2023 13:24:42 -0800 Subject: [PATCH 07/11] Use safe yaml parse in list producer (#4632) --- .../resources/resource_mapping_producer.py | 5 ++-- .../list/resources/test_resources_context.py | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/samcli/lib/list/resources/resource_mapping_producer.py b/samcli/lib/list/resources/resource_mapping_producer.py index 89b96662a5..8f6ef9d0d9 100644 --- a/samcli/lib/list/resources/resource_mapping_producer.py +++ b/samcli/lib/list/resources/resource_mapping_producer.py @@ -4,7 +4,6 @@ from typing import Any, Dict import dataclasses import logging -import yaml from botocore.exceptions import ClientError, NoCredentialsError, BotoCoreError from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader @@ -26,6 +25,7 @@ from samcli.commands.exceptions import UserException from samcli.commands._utils.template import get_template_data from samcli.lib.utils.boto_utils import get_client_error_code +from samcli.yamlhelper import yaml_parse LOG = logging.getLogger(__name__) @@ -98,8 +98,7 @@ def get_translated_dict(self, template_file_dict: Dict[Any, Any]) -> Dict[Any, A validator = SamTemplateValidator( template_file_dict, ManagedPolicyLoader(self.iam_client), profile=self.profile, region=self.region ) - translated_dict: dict - translated_dict = yaml.load(validator.get_translated_template_if_valid(), Loader=yaml.FullLoader) + translated_dict = yaml_parse(validator.get_translated_template_if_valid()) return translated_dict except InvalidSamDocumentException as e: raise InvalidSamTemplateException(str(e)) from e diff --git a/tests/unit/commands/list/resources/test_resources_context.py b/tests/unit/commands/list/resources/test_resources_context.py index 2d67053f37..cd86ecaba6 100644 --- a/tests/unit/commands/list/resources/test_resources_context.py +++ b/tests/unit/commands/list/resources/test_resources_context.py @@ -415,6 +415,35 @@ def test_get_translated_dict_no_region_found_exception( ) resource_producer.get_translated_dict(mock_sam_file_reader.return_value) + @patch("samcli.commands.list.json_consumer.click.echo") + @patch("samcli.commands.list.json_consumer.click.get_current_context") + @patch("samcli.lib.list.resources.resource_mapping_producer.SamTemplateValidator.get_translated_template_if_valid") + @patch("samcli.lib.list.resources.resource_mapping_producer.yaml_parse") + @patch("samcli.lib.list.resources.resource_mapping_producer.get_template_data") + def test_get_translated_dict_calls_safe_yaml_parse( + self, + mock_sam_file_reader, + mock_yaml_parse, + mock_validate_template, + patched_click_get_current_context, + patched_click_echo, + ): + + mock_sam_file_reader.return_value = SAM_FILE_READER_RETURN + resource_producer = ResourceMappingProducer( + stack_name=None, + region=None, + profile=None, + template_file=None, + cloudformation_client=None, + iam_client=None, + mapper=DataToJsonMapper(), + consumer=StringConsumerJsonOutput(), + ) + resource_producer.get_translated_dict(mock_sam_file_reader.return_value) + + mock_yaml_parse.assert_called_once() + class TestResourcesInitClients(TestCase): @patch("samcli.commands.list.json_consumer.click.echo") From 11beb7a308135f2ec434cfbebedae38e1d85b538 Mon Sep 17 00:00:00 2001 From: Qingchuan Ma <69653965+qingchm@users.noreply.github.com> Date: Mon, 30 Jan 2023 14:27:53 -0800 Subject: [PATCH 08/11] Revert an integration test change related to permission change revert (#4633) --- tests/integration/sync/test_sync_adl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/sync/test_sync_adl.py b/tests/integration/sync/test_sync_adl.py index 7e6f84b117..5910fd5d70 100644 --- a/tests/integration/sync/test_sync_adl.py +++ b/tests/integration/sync/test_sync_adl.py @@ -133,7 +133,7 @@ def test_sync_watch_code(self): ) read_until_string( self.watch_process, - "\x1b[32mFinished syncing Layer HelloWorldFunction", + "\x1b[32mFinished syncing Function Layer Reference Sync HelloWorldFunction.\x1b[0m\n", timeout=60, ) lambda_response = json.loads(self._get_lambda_response(lambda_functions[0])) From 075a252f13c338bde3c6d9051776c25b7a03bf41 Mon Sep 17 00:00:00 2001 From: Lucas <12496191+lucashuy@users.noreply.github.com> Date: Wed, 1 Feb 2023 09:32:41 -0800 Subject: [PATCH 09/11] feat: Added feature branches to build and codeql actions (#4643) --- .github/workflows/build.yml | 1 + .github/workflows/codeql.yml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2901ec348..36fff8dbd1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - develop + - "feat/*" jobs: build: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2680639013..a7fbb82f7f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: [ "develop", master ] + branches: [ "develop", master, "feat/*" ] pull_request: # The branches below must be a subset of the branches above - branches: [ "develop" ] + branches: [ "develop", "feat/*" ] schedule: - cron: '24 18 * * 0' From 64e2e4e9dfa9bfe554d6982e10dfb372b7cc2020 Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Wed, 1 Feb 2023 11:08:45 -0800 Subject: [PATCH 10/11] feat: Only stream command output to stdout (#4650) --- samcli/commands/list/cli_common/options.py | 10 ++++++++-- tests/unit/commands/list/test_options.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 tests/unit/commands/list/test_options.py diff --git a/samcli/commands/list/cli_common/options.py b/samcli/commands/list/cli_common/options.py index 2756542594..bf4890099e 100644 --- a/samcli/commands/list/cli_common/options.py +++ b/samcli/commands/list/cli_common/options.py @@ -34,9 +34,15 @@ def output_option(f): return output_click_option()(f) +STACK_NAME_WARNING_MESSAGE = ( + "The --stack-name options was not provided, displaying only local template data. " + "To see data about deployed resources, provide the corresponding stack name." +) + + def stack_name_not_provided_message(): click.secho( fg="yellow", - message="The --stack-name options was not provided, displaying only local template data. " - "To see data about deployed resources, provide the corresponding stack name.", + message=STACK_NAME_WARNING_MESSAGE, + err=True, ) diff --git a/tests/unit/commands/list/test_options.py b/tests/unit/commands/list/test_options.py new file mode 100644 index 0000000000..8040aee5e7 --- /dev/null +++ b/tests/unit/commands/list/test_options.py @@ -0,0 +1,15 @@ +from unittest import TestCase +from unittest.mock import patch + +from samcli.commands.list.cli_common.options import stack_name_not_provided_message, STACK_NAME_WARNING_MESSAGE + + +class TestCommonOptions(TestCase): + @patch("samcli.commands.list.cli_common.options.click") + def test_echoes_warning_messages(self, mock_click): + stack_name_not_provided_message() + mock_click.secho.assert_called_once_with( + fg="yellow", + message=STACK_NAME_WARNING_MESSAGE, + err=True, + ) From f8b2e60f37f90864cd01d671cfa136bee5bba28e Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Wed, 1 Feb 2023 16:13:02 -0800 Subject: [PATCH 11/11] chore: bump version to 1.72.0 (#4652) --- samcli/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samcli/__init__.py b/samcli/__init__.py index e6b7fc020c..d277adead1 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = "1.71.0" +__version__ = "1.72.0"