From dd9d814f8b748e955bea948b545190a5739837ba Mon Sep 17 00:00:00 2001 From: Tarun Date: Mon, 8 Nov 2021 09:38:40 -0800 Subject: [PATCH 01/59] Fix no allowed origin (#2180) * Fixing case when no allowed origin is passed. * Adding functional tests to verify proper error message. Co-authored-by: Tarun Mall --- samtranslator/model/api/api_generator.py | 19 +++-- samtranslator/swagger/swagger.py | 2 +- tests/swagger/test_swagger.py | 4 +- ..._api_with_cors_and_empty_allow_origin.yaml | 80 +++++++++++++++++++ ..._api_with_cors_and_empty_allow_origin.json | 8 ++ 5 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 tests/translator/input/error_api_with_cors_and_empty_allow_origin.yaml create mode 100644 tests/translator/output/error_api_with_cors_and_empty_allow_origin.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index ac02c33ef..fba368f8a 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -603,14 +603,17 @@ def _add_cors(self): editor = SwaggerEditor(self.definition_body) for path in editor.iter_on_path(): - editor.add_cors( - path, - properties.AllowOrigin, - properties.AllowHeaders, - properties.AllowMethods, - max_age=properties.MaxAge, - allow_credentials=properties.AllowCredentials, - ) + try: + editor.add_cors( + path, + properties.AllowOrigin, + properties.AllowHeaders, + properties.AllowMethods, + max_age=properties.MaxAge, + allow_credentials=properties.AllowCredentials, + ) + except InvalidTemplateException as ex: + raise InvalidResourceException(self.logical_id, ex.message) # Assign the Swagger back to template self.definition_body = editor.swagger diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 50c974282..5718c5d0f 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -321,7 +321,7 @@ def add_cors( return if not allowed_origins: - raise ValueError("Invalid input. Value for AllowedOrigins is required") + raise InvalidTemplateException("Invalid input. Value for AllowedOrigins is required") if not allowed_methods: # AllowMethods is not given. Let's try to generate the list from the given Swagger. diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index ee578be33..ec33f7d86 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -5,7 +5,7 @@ from parameterized import parameterized, param from samtranslator.swagger.swagger import SwaggerEditor -from samtranslator.model.exceptions import InvalidDocumentException +from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException from tests.translator.test_translator import deep_sort_lists _X_INTEGRATION = "x-amazon-apigateway-integration" @@ -352,7 +352,7 @@ def test_must_fail_with_bad_values_for_path(self): def test_must_fail_for_invalid_allowed_origin(self): path = "/foo" - with self.assertRaises(ValueError): + with self.assertRaises(InvalidTemplateException): self.editor.add_cors(path, None, "headers", "methods") def test_must_work_for_optional_allowed_headers(self): diff --git a/tests/translator/input/error_api_with_cors_and_empty_allow_origin.yaml b/tests/translator/input/error_api_with_cors_and_empty_allow_origin.yaml new file mode 100644 index 000000000..48c872863 --- /dev/null +++ b/tests/translator/input/error_api_with_cors_and_empty_allow_origin.yaml @@ -0,0 +1,80 @@ +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs12.x + Events: + GetHtml: + Type: Api + Properties: + Path: / + Method: get + AnyApi: + Type: Api + Properties: + Path: /foo + Method: any + RestApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs12.x + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs12.x + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/add": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } + Cors: + AllowMethods: "methods" + AllowHeaders: "headers" + AllowOrigin: "" + AllowCredentials: true diff --git a/tests/translator/output/error_api_with_cors_and_empty_allow_origin.json b/tests/translator/output/error_api_with_cors_and_empty_allow_origin.json new file mode 100644 index 000000000..a02f7b505 --- /dev/null +++ b/tests/translator/output/error_api_with_cors_and_empty_allow_origin.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [ExplicitApi] is invalid. Structure of the SAM template is invalid. Invalid input. Value for AllowedOrigins is required" + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [ExplicitApi] is invalid. Structure of the SAM template is invalid. Invalid input. Value for AllowedOrigins is required" +} From ce2744dfd912d14e431ceb58adab169c8d9849b5 Mon Sep 17 00:00:00 2001 From: _sam <3804518+aahung@users.noreply.github.com> Date: Mon, 8 Nov 2021 17:21:47 -0800 Subject: [PATCH 02/59] Raise InvalidEventException when RestApiId/ApiId is not resolved to a string (#2213) --- .../plugins/api/implicit_http_api_plugin.py | 8 +++++--- .../plugins/api/implicit_rest_api_plugin.py | 8 +++++--- .../error_function_invalid_event_api_ref.yaml | 16 ++++++++++++++++ ...ror_function_invalid_event_http_api_ref.yaml | 17 +++++++++++++++++ .../error_api_event_import_vaule_reference.json | 2 +- ...gration_with_condition_intrinsic_api_id.json | 2 +- ...ation_with_find_in_map_intrinsic_api_id.json | 2 +- ...ntegration_with_getatt_intrinsic_api_id.json | 2 +- ..._integration_with_join_intrinsic_api_id.json | 2 +- ...ntegration_with_select_intrinsic_api_id.json | 2 +- ...r_integration_with_sub_intrinsic_api_id.json | 2 +- ...gration_with_transform_intrinsic_api_id.json | 2 +- .../error_function_invalid_event_api_ref.json | 8 ++++++++ ...ror_function_invalid_event_http_api_ref.json | 8 ++++++++ 14 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 tests/translator/input/error_function_invalid_event_api_ref.yaml create mode 100644 tests/translator/input/error_function_invalid_event_http_api_ref.yaml create mode 100644 tests/translator/output/error_function_invalid_event_api_ref.json create mode 100644 tests/translator/output/error_function_invalid_event_http_api_ref.json diff --git a/samtranslator/plugins/api/implicit_http_api_plugin.py b/samtranslator/plugins/api/implicit_http_api_plugin.py index 372742632..5012ac11f 100644 --- a/samtranslator/plugins/api/implicit_http_api_plugin.py +++ b/samtranslator/plugins/api/implicit_http_api_plugin.py @@ -87,9 +87,11 @@ def _process_api_events( key = "Path" if not isinstance(path, six.string_types) else "Method" raise InvalidEventException(logicalId, "Api Event must have a String specified for '{}'.".format(key)) - # !Ref is resolved by this time. If it is still a dict, we can't parse/use this Api. - if isinstance(api_id, dict): - raise InvalidEventException(logicalId, "Api Event must reference an Api in the same template.") + # !Ref is resolved by this time. If it is not a string, we can't parse/use this Api. + if api_id and not isinstance(api_id, six.string_types): + raise InvalidEventException( + logicalId, "Api Event's ApiId must be a string referencing an Api in the same template." + ) api_dict_condition = self.api_conditions.setdefault(api_id, {}) method_conditions = api_dict_condition.setdefault(path, {}) diff --git a/samtranslator/plugins/api/implicit_rest_api_plugin.py b/samtranslator/plugins/api/implicit_rest_api_plugin.py index 3e94309f1..5636abbf2 100644 --- a/samtranslator/plugins/api/implicit_rest_api_plugin.py +++ b/samtranslator/plugins/api/implicit_rest_api_plugin.py @@ -85,9 +85,11 @@ def _process_api_events( if not isinstance(method, six.string_types): raise InvalidEventException(logicalId, "Api Event must have a String specified for 'Method'.") - # !Ref is resolved by this time. If it is still a dict, we can't parse/use this Api. - if isinstance(api_id, dict): - raise InvalidEventException(logicalId, "Api Event must reference an Api in the same template.") + # !Ref is resolved by this time. If it is not a string, we can't parse/use this Api. + if api_id and not isinstance(api_id, six.string_types): + raise InvalidEventException( + logicalId, "Api Event's RestApiId must be a string referencing an Api in the same template." + ) api_dict_condition = self.api_conditions.setdefault(api_id, {}) method_conditions = api_dict_condition.setdefault(path, {}) diff --git a/tests/translator/input/error_function_invalid_event_api_ref.yaml b/tests/translator/input/error_function_invalid_event_api_ref.yaml new file mode 100644 index 000000000..ac7052f52 --- /dev/null +++ b/tests/translator/input/error_function_invalid_event_api_ref.yaml @@ -0,0 +1,16 @@ +Resources: + FunctionApiRestApiRefError: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Events: + ApiEvent: + Type: Api + Properties: + Method: get + Path: / + # RestApiId should not be a list + RestApiId: + - Fn::Sub: ServerlessRestApi diff --git a/tests/translator/input/error_function_invalid_event_http_api_ref.yaml b/tests/translator/input/error_function_invalid_event_http_api_ref.yaml new file mode 100644 index 000000000..fb05dc323 --- /dev/null +++ b/tests/translator/input/error_function_invalid_event_http_api_ref.yaml @@ -0,0 +1,17 @@ +Resources: + FunctionApiHttpApiRefError: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + Events: + ApiEvent: + Type: HttpApi + Properties: + Method: get + Path: / + # ApiId should not be a list + ApiId: + - Fn::Sub: ServerlessHttpApi + diff --git a/tests/translator/output/error_api_event_import_vaule_reference.json b/tests/translator/output/error_api_event_import_vaule_reference.json index fa85df16b..83b444b46 100644 --- a/tests/translator/output/error_api_event_import_vaule_reference.json +++ b/tests/translator/output/error_api_event_import_vaule_reference.json @@ -1 +1 @@ -{"errorMessage":"Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Function] is invalid. Event with id [GetHtml] is invalid. Api Event must reference an Api in the same template."} \ No newline at end of file +{"errorMessage":"Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Function] is invalid. Event with id [GetHtml] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template."} \ No newline at end of file diff --git a/tests/translator/output/error_api_swagger_integration_with_condition_intrinsic_api_id.json b/tests/translator/output/error_api_swagger_integration_with_condition_intrinsic_api_id.json index ffefd8463..fd1abe8a3 100644 --- a/tests/translator/output/error_api_swagger_integration_with_condition_intrinsic_api_id.json +++ b/tests/translator/output/error_api_swagger_integration_with_condition_intrinsic_api_id.json @@ -1,4 +1,4 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event must reference an Api in the same template." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." } \ No newline at end of file diff --git a/tests/translator/output/error_api_swagger_integration_with_find_in_map_intrinsic_api_id.json b/tests/translator/output/error_api_swagger_integration_with_find_in_map_intrinsic_api_id.json index ffefd8463..fd1abe8a3 100644 --- a/tests/translator/output/error_api_swagger_integration_with_find_in_map_intrinsic_api_id.json +++ b/tests/translator/output/error_api_swagger_integration_with_find_in_map_intrinsic_api_id.json @@ -1,4 +1,4 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event must reference an Api in the same template." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." } \ No newline at end of file diff --git a/tests/translator/output/error_api_swagger_integration_with_getatt_intrinsic_api_id.json b/tests/translator/output/error_api_swagger_integration_with_getatt_intrinsic_api_id.json index ffefd8463..fd1abe8a3 100644 --- a/tests/translator/output/error_api_swagger_integration_with_getatt_intrinsic_api_id.json +++ b/tests/translator/output/error_api_swagger_integration_with_getatt_intrinsic_api_id.json @@ -1,4 +1,4 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event must reference an Api in the same template." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." } \ No newline at end of file diff --git a/tests/translator/output/error_api_swagger_integration_with_join_intrinsic_api_id.json b/tests/translator/output/error_api_swagger_integration_with_join_intrinsic_api_id.json index ffefd8463..fd1abe8a3 100644 --- a/tests/translator/output/error_api_swagger_integration_with_join_intrinsic_api_id.json +++ b/tests/translator/output/error_api_swagger_integration_with_join_intrinsic_api_id.json @@ -1,4 +1,4 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event must reference an Api in the same template." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." } \ No newline at end of file diff --git a/tests/translator/output/error_api_swagger_integration_with_select_intrinsic_api_id.json b/tests/translator/output/error_api_swagger_integration_with_select_intrinsic_api_id.json index ffefd8463..fd1abe8a3 100644 --- a/tests/translator/output/error_api_swagger_integration_with_select_intrinsic_api_id.json +++ b/tests/translator/output/error_api_swagger_integration_with_select_intrinsic_api_id.json @@ -1,4 +1,4 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event must reference an Api in the same template." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." } \ No newline at end of file diff --git a/tests/translator/output/error_api_swagger_integration_with_sub_intrinsic_api_id.json b/tests/translator/output/error_api_swagger_integration_with_sub_intrinsic_api_id.json index ffefd8463..fd1abe8a3 100644 --- a/tests/translator/output/error_api_swagger_integration_with_sub_intrinsic_api_id.json +++ b/tests/translator/output/error_api_swagger_integration_with_sub_intrinsic_api_id.json @@ -1,4 +1,4 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event must reference an Api in the same template." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." } \ No newline at end of file diff --git a/tests/translator/output/error_api_swagger_integration_with_transform_intrinsic_api_id.json b/tests/translator/output/error_api_swagger_integration_with_transform_intrinsic_api_id.json index ffefd8463..fd1abe8a3 100644 --- a/tests/translator/output/error_api_swagger_integration_with_transform_intrinsic_api_id.json +++ b/tests/translator/output/error_api_swagger_integration_with_transform_intrinsic_api_id.json @@ -1,4 +1,4 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event must reference an Api in the same template." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [HtmlFunction] is invalid. Event with id [GetHtml] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." } \ No newline at end of file diff --git a/tests/translator/output/error_function_invalid_event_api_ref.json b/tests/translator/output/error_function_invalid_event_api_ref.json new file mode 100644 index 000000000..b02fdaa75 --- /dev/null +++ b/tests/translator/output/error_function_invalid_event_api_ref.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [FunctionApiRestApiRefError] is invalid. Event with id [ApiEvent] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [FunctionApiRestApiRefError] is invalid. Event with id [ApiEvent] is invalid. Api Event's RestApiId must be a string referencing an Api in the same template." +} diff --git a/tests/translator/output/error_function_invalid_event_http_api_ref.json b/tests/translator/output/error_function_invalid_event_http_api_ref.json new file mode 100644 index 000000000..466a4bf4f --- /dev/null +++ b/tests/translator/output/error_function_invalid_event_http_api_ref.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [FunctionApiHttpApiRefError] is invalid. Event with id [ApiEvent] is invalid. Api Event's ApiId must be a string referencing an Api in the same template." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [FunctionApiHttpApiRefError] is invalid. Event with id [ApiEvent] is invalid. Api Event's ApiId must be a string referencing an Api in the same template." +} From 3c2da6d8c4274fe6c42a9cb4264f23f769256cff Mon Sep 17 00:00:00 2001 From: Mohamed Elasmar <71043312+moelasmar@users.noreply.github.com> Date: Mon, 8 Nov 2021 17:44:33 -0800 Subject: [PATCH 03/59] Raise a validation exception if the responses section in an openApi definition path is not a dictionary object (#2214) * raise a validation exception if the responses section in options method for a path in OpenApi Definition * black formating * validate the responses section should be a dictionary not only not null. * fix py27 issue --- samtranslator/model/api/api_generator.py | 11 ++++++++++- .../input/error_api_invalid_openapi_path.yaml | 13 +++++++++++++ ...invalid_openapi_path_with_empty_responses.yaml | 15 +++++++++++++++ ...nvalid_openapi_path_with_string_responses.yaml | 15 +++++++++++++++ .../output/error_api_invalid_openapi_path.json | 3 +++ ...invalid_openapi_path_with_empty_responses.json | 3 +++ ...nvalid_openapi_path_with_string_responses.json | 3 +++ 7 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/translator/input/error_api_invalid_openapi_path.yaml create mode 100644 tests/translator/input/error_api_invalid_openapi_path_with_empty_responses.yaml create mode 100644 tests/translator/input/error_api_invalid_openapi_path_with_string_responses.yaml create mode 100644 tests/translator/output/error_api_invalid_openapi_path.json create mode 100644 tests/translator/output/error_api_invalid_openapi_path_with_empty_responses.json create mode 100644 tests/translator/output/error_api_invalid_openapi_path_with_string_responses.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index fba368f8a..04cf78e74 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -18,7 +18,7 @@ ApiGatewayApiKey, ) from samtranslator.model.route53 import Route53RecordSetGroup -from samtranslator.model.exceptions import InvalidResourceException, InvalidTemplateException +from samtranslator.model.exceptions import InvalidResourceException, InvalidTemplateException, InvalidDocumentException from samtranslator.model.s3_utils.uri_parser import parse_s3_uri from samtranslator.region_configuration import RegionConfiguration from samtranslator.swagger.swagger import SwaggerEditor @@ -980,6 +980,15 @@ def _openapi_postprocess(self, definition_body): # add schema for the headers in options section for openapi3 if field in ["responses"]: options_path = definition_body["paths"][path]["options"] + if options_path and not isinstance(options_path.get(field), dict): + raise InvalidDocumentException( + [ + InvalidTemplateException( + "Value of responses in options method for path {} must be a " + "dictionary according to Swagger spec.".format(path) + ) + ] + ) if ( options_path and options_path.get(field).get("200") diff --git a/tests/translator/input/error_api_invalid_openapi_path.yaml b/tests/translator/input/error_api_invalid_openapi_path.yaml new file mode 100644 index 000000000..db21fc2c2 --- /dev/null +++ b/tests/translator/input/error_api_invalid_openapi_path.yaml @@ -0,0 +1,13 @@ +Resources: + ApiWithInvalidPath: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + Cors: "'*'" + OpenApiVersion: 3.0.1 + DefinitionBody: + openapi: 3.0.1 + info: + title: test invalid paths Api + paths: + /foo: \ No newline at end of file diff --git a/tests/translator/input/error_api_invalid_openapi_path_with_empty_responses.yaml b/tests/translator/input/error_api_invalid_openapi_path_with_empty_responses.yaml new file mode 100644 index 000000000..b2a2e5236 --- /dev/null +++ b/tests/translator/input/error_api_invalid_openapi_path_with_empty_responses.yaml @@ -0,0 +1,15 @@ +Resources: + ApiWithInvalidPath: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + Cors: "'*'" + OpenApiVersion: 3.0.1 + DefinitionBody: + openapi: 3.0.1 + info: + title: test invalid paths Api + paths: + /foo: + options: + responses: \ No newline at end of file diff --git a/tests/translator/input/error_api_invalid_openapi_path_with_string_responses.yaml b/tests/translator/input/error_api_invalid_openapi_path_with_string_responses.yaml new file mode 100644 index 000000000..596839e45 --- /dev/null +++ b/tests/translator/input/error_api_invalid_openapi_path_with_string_responses.yaml @@ -0,0 +1,15 @@ +Resources: + ApiWithInvalidPath: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + Cors: "'*'" + OpenApiVersion: 3.0.1 + DefinitionBody: + openapi: 3.0.1 + info: + title: test invalid paths Api + paths: + /foo: + options: + responses: invalid \ No newline at end of file diff --git a/tests/translator/output/error_api_invalid_openapi_path.json b/tests/translator/output/error_api_invalid_openapi_path.json new file mode 100644 index 000000000..2aedc0684 --- /dev/null +++ b/tests/translator/output/error_api_invalid_openapi_path.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of '/foo' path must be a dictionary according to Swagger spec." +} \ No newline at end of file diff --git a/tests/translator/output/error_api_invalid_openapi_path_with_empty_responses.json b/tests/translator/output/error_api_invalid_openapi_path_with_empty_responses.json new file mode 100644 index 000000000..73a7f49ac --- /dev/null +++ b/tests/translator/output/error_api_invalid_openapi_path_with_empty_responses.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of responses in options method for path /foo must be a dictionary according to Swagger spec." +} \ No newline at end of file diff --git a/tests/translator/output/error_api_invalid_openapi_path_with_string_responses.json b/tests/translator/output/error_api_invalid_openapi_path_with_string_responses.json new file mode 100644 index 000000000..73a7f49ac --- /dev/null +++ b/tests/translator/output/error_api_invalid_openapi_path_with_string_responses.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of responses in options method for path /foo must be a dictionary according to Swagger spec." +} \ No newline at end of file From cc2805114c564d39c57721ba4c74bed96389ffe7 Mon Sep 17 00:00:00 2001 From: Jacob Fuss <32497805+jfuss@users.noreply.github.com> Date: Mon, 15 Nov 2021 12:07:13 -0600 Subject: [PATCH 04/59] chore: Add auto PR labeler (#2219) --- .github/workflows/README.md | 7 +++++++ .github/workflows/pr-labeler.yml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/pr-labeler.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 000000000..46407007e --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,7 @@ +This folder has Github Actions for this repo. + +** pr-labler ** + +This is responsible for tagging our prs automattically. The primary thing it does is tags internal vs external (to the team) PRs. +This is run on `pull_request_target` which only runs what is in the repo not what is in the Pull Request. This is done to help guard against +a PR running and changing. For this, the Action should NEVER download or checkout the PR. It is purely for tagging/labeling not CI. \ No newline at end of file diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 000000000..c6066bd7a --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,30 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +jobs: + apply-internal-external-label: + permissions: + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v5 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const maintainers = ['jfuss', 'c2tarun', 'hoffa', 'awood45', 'CoshUS', 'aahung', 'hawflau', 'mndeveci', 'ssenchenko', 'wchengru', 'mingkun2020', 'qingchm', 'moelasmar', 'xazhao', 'mildaniel', 'marekaiv', 'torresxb1'] + if (maintainers.includes(context.payload.sender.login)) { + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['pr/internal'] + }) + } else { + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['pr/external'] + }) + } From 1174f0f9497e4056ec033c922a6e77118d958475 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Mon, 15 Nov 2021 10:57:47 -0800 Subject: [PATCH 05/59] Fix DestinationConfig in streaming event sources (#2215) --- samtranslator/model/eventsources/pull.py | 8 ++-- ..._invalid_stream_eventsource_dest_type.yaml | 45 +++++++++++++++++++ ...re_in_stream_event_destination_config.yaml | 45 +++++++++++++++++++ .../function_with_event_source_mapping.yaml | 16 +++++++ ..._invalid_stream_eventsource_dest_type.json | 8 ++++ ...re_in_stream_event_destination_config.json | 8 ++++ .../function_with_event_source_mapping.json | 43 ++++++++++++++++++ ..._invalid_stream_eventsource_dest_type.json | 8 ++++ ...re_in_stream_event_destination_config.json | 8 ++++ .../function_with_event_source_mapping.json | 43 ++++++++++++++++++ ..._invalid_stream_eventsource_dest_type.json | 8 ++++ ...re_in_stream_event_destination_config.json | 8 ++++ .../function_with_event_source_mapping.json | 43 ++++++++++++++++++ 13 files changed, 287 insertions(+), 4 deletions(-) create mode 100644 tests/translator/input/error_function_with_invalid_stream_eventsource_dest_type.yaml create mode 100644 tests/translator/input/error_function_with_missing_on_failure_in_stream_event_destination_config.yaml create mode 100644 tests/translator/output/aws-cn/error_function_with_invalid_stream_eventsource_dest_type.json create mode 100644 tests/translator/output/aws-cn/error_function_with_missing_on_failure_in_stream_event_destination_config.json create mode 100644 tests/translator/output/aws-us-gov/error_function_with_invalid_stream_eventsource_dest_type.json create mode 100644 tests/translator/output/aws-us-gov/error_function_with_missing_on_failure_in_stream_event_destination_config.json create mode 100644 tests/translator/output/error_function_with_invalid_stream_eventsource_dest_type.json create mode 100644 tests/translator/output/error_function_with_missing_on_failure_in_stream_event_destination_config.json diff --git a/samtranslator/model/eventsources/pull.py b/samtranslator/model/eventsources/pull.py index 537d9cd30..6af57a29d 100644 --- a/samtranslator/model/eventsources/pull.py +++ b/samtranslator/model/eventsources/pull.py @@ -110,6 +110,9 @@ def to_cloudformation(self, **kwargs): destination_config_policy = None if self.DestinationConfig: + if self.DestinationConfig.get("OnFailure") is None: + raise InvalidEventException(self.logical_id, "'OnFailure' is a required field for 'DestinationConfig'") + # `Type` property is for sam to attach the right policies destination_type = self.DestinationConfig.get("OnFailure").get("Type") @@ -120,10 +123,6 @@ def to_cloudformation(self, **kwargs): # the values 'SQS' and 'SNS' are allowed. No intrinsics are allowed if destination_type not in ["SQS", "SNS"]: raise InvalidEventException(self.logical_id, "The only valid values for 'Type' are 'SQS' and 'SNS'") - if self.DestinationConfig.get("OnFailure") is None: - raise InvalidEventException( - self.logical_id, "'OnFailure' is a required field for " "'DestinationConfig'" - ) if destination_type == "SQS": queue_arn = self.DestinationConfig.get("OnFailure").get("Destination") destination_config_policy = IAMRolePolicies().sqs_send_message_role_policy( @@ -134,6 +133,7 @@ def to_cloudformation(self, **kwargs): destination_config_policy = IAMRolePolicies().sns_publish_role_policy( sns_topic_arn, self.logical_id ) + lambda_eventsourcemapping.DestinationConfig = self.DestinationConfig if "role" in kwargs: diff --git a/tests/translator/input/error_function_with_invalid_stream_eventsource_dest_type.yaml b/tests/translator/input/error_function_with_invalid_stream_eventsource_dest_type.yaml new file mode 100644 index 000000000..ddbde8b65 --- /dev/null +++ b/tests/translator/input/error_function_with_invalid_stream_eventsource_dest_type.yaml @@ -0,0 +1,45 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Parameters: + MyBatchingWindowParam: + Type: Number + Default: 45 + Description: parameter for batching window in seconds + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + InlineCode: | + exports.handler = async (event) => { + return { + statusCode: 200, + body: JSON.stringify(event), + headers: {} + } + } + Runtime: nodejs12.x + Policies: + - SQSSendMessagePolicy: + QueueName: !GetAtt MySqsQueue.QueueName + Events: + StreamEvent: + Type: Kinesis + Properties: + Stream: !GetAtt KinesisStream.Arn + MaximumBatchingWindowInSeconds: !Ref MyBatchingWindowParam + StartingPosition: LATEST + DestinationConfig: + OnFailure: + Type: INVALID_VALID + Destination: !Ref MySnsTopic + + KinesisStream: + Type: AWS::Kinesis::Stream + Properties: + ShardCount: 1 + + MySnsTopic: + Type: AWS::SNS::Topic \ No newline at end of file diff --git a/tests/translator/input/error_function_with_missing_on_failure_in_stream_event_destination_config.yaml b/tests/translator/input/error_function_with_missing_on_failure_in_stream_event_destination_config.yaml new file mode 100644 index 000000000..67510e2c3 --- /dev/null +++ b/tests/translator/input/error_function_with_missing_on_failure_in_stream_event_destination_config.yaml @@ -0,0 +1,45 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Parameters: + MyBatchingWindowParam: + Type: Number + Default: 45 + Description: parameter for batching window in seconds + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + InlineCode: | + exports.handler = async (event) => { + return { + statusCode: 200, + body: JSON.stringify(event), + headers: {} + } + } + Runtime: nodejs12.x + Policies: + - SQSSendMessagePolicy: + QueueName: !GetAtt MySqsQueue.QueueName + Events: + StreamEvent: + Type: Kinesis + Properties: + Stream: !GetAtt KinesisStream.Arn + MaximumBatchingWindowInSeconds: !Ref MyBatchingWindowParam + StartingPosition: LATEST + DestinationConfig: + InvalidConfig: + Type: SNS + Destination: !Ref MySnsTopic + + KinesisStream: + Type: AWS::Kinesis::Stream + Properties: + ShardCount: 1 + + MySnsTopic: + Type: AWS::SNS::Topic \ No newline at end of file diff --git a/tests/translator/input/function_with_event_source_mapping.yaml b/tests/translator/input/function_with_event_source_mapping.yaml index edea3e214..7a55afa02 100644 --- a/tests/translator/input/function_with_event_source_mapping.yaml +++ b/tests/translator/input/function_with_event_source_mapping.yaml @@ -59,6 +59,22 @@ Resources: OnFailure: Type: SQS Destination: !GetAtt MySqsQueue.Arn + StreamEventWithoutDestinationConfigType: + Type: Kinesis + Properties: + Stream: !GetAtt KinesisStream1.Arn + MaximumBatchingWindowInSeconds: !Ref MyBatchingWindowParam + StartingPosition: LATEST + DestinationConfig: + OnFailure: + Destination: !Ref MySnsTopic + StreamEventWithEmptyDestinationConfig: + Type: Kinesis + Properties: + Stream: !GetAtt KinesisStream1.Arn + MaximumBatchingWindowInSeconds: !Ref MyBatchingWindowParam + StartingPosition: LATEST + DestinationConfig: KinesisStream: Type: AWS::Kinesis::Stream diff --git a/tests/translator/output/aws-cn/error_function_with_invalid_stream_eventsource_dest_type.json b/tests/translator/output/aws-cn/error_function_with_invalid_stream_eventsource_dest_type.json new file mode 100644 index 000000000..dc4f300bd --- /dev/null +++ b/tests/translator/output/aws-cn/error_function_with_invalid_stream_eventsource_dest_type.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. The only valid values for 'Type' are 'SQS' and 'SNS'" + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. The only valid values for 'Type' are 'SQS' and 'SNS'" +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/error_function_with_missing_on_failure_in_stream_event_destination_config.json b/tests/translator/output/aws-cn/error_function_with_missing_on_failure_in_stream_event_destination_config.json new file mode 100644 index 000000000..dcc5d33fb --- /dev/null +++ b/tests/translator/output/aws-cn/error_function_with_missing_on_failure_in_stream_event_destination_config.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. 'OnFailure' is a required field for 'DestinationConfig'" + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. 'OnFailure' is a required field for 'DestinationConfig'" +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/function_with_event_source_mapping.json b/tests/translator/output/aws-cn/function_with_event_source_mapping.json index c566b9490..0a042dd07 100644 --- a/tests/translator/output/aws-cn/function_with_event_source_mapping.json +++ b/tests/translator/output/aws-cn/function_with_event_source_mapping.json @@ -233,6 +233,49 @@ } } }, + "MyFunctionForBatchingExampleStreamEventWithoutDestinationConfigType": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "MaximumBatchingWindowInSeconds": { + "Ref": "MyBatchingWindowParam" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "KinesisStream1", + "Arn" + ] + }, + "FunctionName": { + "Ref": "MyFunctionForBatchingExample" + }, + "StartingPosition": "LATEST", + "DestinationConfig": { + "OnFailure": { + "Destination": { + "Ref": "MySnsTopic" + } + } + } + } + }, + "MyFunctionForBatchingExampleStreamEventWithEmptyDestinationConfig": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "MaximumBatchingWindowInSeconds": { + "Ref": "MyBatchingWindowParam" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "KinesisStream1", + "Arn" + ] + }, + "FunctionName": { + "Ref": "MyFunctionForBatchingExample" + }, + "StartingPosition": "LATEST" + } + }, "KinesisStream": { "Type": "AWS::Kinesis::Stream", "Properties": { diff --git a/tests/translator/output/aws-us-gov/error_function_with_invalid_stream_eventsource_dest_type.json b/tests/translator/output/aws-us-gov/error_function_with_invalid_stream_eventsource_dest_type.json new file mode 100644 index 000000000..dc4f300bd --- /dev/null +++ b/tests/translator/output/aws-us-gov/error_function_with_invalid_stream_eventsource_dest_type.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. The only valid values for 'Type' are 'SQS' and 'SNS'" + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. The only valid values for 'Type' are 'SQS' and 'SNS'" +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/error_function_with_missing_on_failure_in_stream_event_destination_config.json b/tests/translator/output/aws-us-gov/error_function_with_missing_on_failure_in_stream_event_destination_config.json new file mode 100644 index 000000000..dcc5d33fb --- /dev/null +++ b/tests/translator/output/aws-us-gov/error_function_with_missing_on_failure_in_stream_event_destination_config.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. 'OnFailure' is a required field for 'DestinationConfig'" + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. 'OnFailure' is a required field for 'DestinationConfig'" +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/function_with_event_source_mapping.json b/tests/translator/output/aws-us-gov/function_with_event_source_mapping.json index a910aefed..554891d3f 100644 --- a/tests/translator/output/aws-us-gov/function_with_event_source_mapping.json +++ b/tests/translator/output/aws-us-gov/function_with_event_source_mapping.json @@ -233,6 +233,49 @@ } } }, + "MyFunctionForBatchingExampleStreamEventWithoutDestinationConfigType": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "MaximumBatchingWindowInSeconds": { + "Ref": "MyBatchingWindowParam" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "KinesisStream1", + "Arn" + ] + }, + "FunctionName": { + "Ref": "MyFunctionForBatchingExample" + }, + "StartingPosition": "LATEST", + "DestinationConfig": { + "OnFailure": { + "Destination": { + "Ref": "MySnsTopic" + } + } + } + } + }, + "MyFunctionForBatchingExampleStreamEventWithEmptyDestinationConfig": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "MaximumBatchingWindowInSeconds": { + "Ref": "MyBatchingWindowParam" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "KinesisStream1", + "Arn" + ] + }, + "FunctionName": { + "Ref": "MyFunctionForBatchingExample" + }, + "StartingPosition": "LATEST" + } + }, "KinesisStream": { "Type": "AWS::Kinesis::Stream", "Properties": { diff --git a/tests/translator/output/error_function_with_invalid_stream_eventsource_dest_type.json b/tests/translator/output/error_function_with_invalid_stream_eventsource_dest_type.json new file mode 100644 index 000000000..dc4f300bd --- /dev/null +++ b/tests/translator/output/error_function_with_invalid_stream_eventsource_dest_type.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. The only valid values for 'Type' are 'SQS' and 'SNS'" + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. The only valid values for 'Type' are 'SQS' and 'SNS'" +} \ No newline at end of file diff --git a/tests/translator/output/error_function_with_missing_on_failure_in_stream_event_destination_config.json b/tests/translator/output/error_function_with_missing_on_failure_in_stream_event_destination_config.json new file mode 100644 index 000000000..dcc5d33fb --- /dev/null +++ b/tests/translator/output/error_function_with_missing_on_failure_in_stream_event_destination_config.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. 'OnFailure' is a required field for 'DestinationConfig'" + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyFunction] is invalid. Event with id [MyFunctionStreamEvent] is invalid. 'OnFailure' is a required field for 'DestinationConfig'" +} \ No newline at end of file diff --git a/tests/translator/output/function_with_event_source_mapping.json b/tests/translator/output/function_with_event_source_mapping.json index 2a2f668ef..fcff5cabc 100644 --- a/tests/translator/output/function_with_event_source_mapping.json +++ b/tests/translator/output/function_with_event_source_mapping.json @@ -233,6 +233,49 @@ } } }, + "MyFunctionForBatchingExampleStreamEventWithoutDestinationConfigType": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "MaximumBatchingWindowInSeconds": { + "Ref": "MyBatchingWindowParam" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "KinesisStream1", + "Arn" + ] + }, + "FunctionName": { + "Ref": "MyFunctionForBatchingExample" + }, + "StartingPosition": "LATEST", + "DestinationConfig": { + "OnFailure": { + "Destination": { + "Ref": "MySnsTopic" + } + } + } + } + }, + "MyFunctionForBatchingExampleStreamEventWithEmptyDestinationConfig": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "MaximumBatchingWindowInSeconds": { + "Ref": "MyBatchingWindowParam" + }, + "EventSourceArn": { + "Fn::GetAtt": [ + "KinesisStream1", + "Arn" + ] + }, + "FunctionName": { + "Ref": "MyFunctionForBatchingExample" + }, + "StartingPosition": "LATEST" + } + }, "KinesisStream": { "Type": "AWS::Kinesis::Stream", "Properties": { From c8e4da9f259fcc9fecc6516e830b445d32f95259 Mon Sep 17 00:00:00 2001 From: Ruperto Torres <86501267+torresxb1@users.noreply.github.com> Date: Mon, 15 Nov 2021 13:09:22 -0800 Subject: [PATCH 06/59] fix: handle non-dict DefinitionBody path item in _openapi_postprocess (#2216) * handle null DefinitionBody path item and null options field value, plus some reformatting * check dictionary type * more refactoring + throw error if not dict --- samtranslator/model/api/api_generator.py | 45 ++++++-------- samtranslator/swagger/swagger.py | 61 ++++++++++--------- ...api_no_cors_invalid_openapi_null_path.yaml | 12 ++++ ...i_no_cors_invalid_openapi_string_path.yaml | 12 ++++ ...api_no_cors_invalid_openapi_null_path.json | 3 + ...i_no_cors_invalid_openapi_string_path.json | 3 + 6 files changed, 80 insertions(+), 56 deletions(-) create mode 100644 tests/translator/input/error_api_no_cors_invalid_openapi_null_path.yaml create mode 100644 tests/translator/input/error_api_no_cors_invalid_openapi_string_path.yaml create mode 100644 tests/translator/output/error_api_no_cors_invalid_openapi_null_path.json create mode 100644 tests/translator/output/error_api_no_cors_invalid_openapi_string_path.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 04cf78e74..e07117e4d 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -969,41 +969,30 @@ def _openapi_postprocess(self, definition_body): del definition_body["definitions"] # removes `consumes` and `produces` options for CORS in openapi3 and # adds `schema` for the headers in responses for openapi3 - if definition_body.get("paths"): - for path in definition_body.get("paths"): - if definition_body.get("paths").get(path).get("options"): - definition_body_options = definition_body.get("paths").get(path).get("options").copy() - for field in definition_body_options.keys(): + paths = definition_body.get("paths") + if paths: + for path, path_item in paths.items(): + SwaggerEditor.validate_path_item_is_dict(path_item, path) + if path_item.get("options"): + options = path_item.get("options").copy() + for field, field_val in options.items(): # remove unsupported produces and consumes in options for openapi3 if field in ["produces", "consumes"]: del definition_body["paths"][path]["options"][field] # add schema for the headers in options section for openapi3 if field in ["responses"]: - options_path = definition_body["paths"][path]["options"] - if options_path and not isinstance(options_path.get(field), dict): - raise InvalidDocumentException( - [ - InvalidTemplateException( - "Value of responses in options method for path {} must be a " - "dictionary according to Swagger spec.".format(path) - ) - ] - ) - if ( - options_path - and options_path.get(field).get("200") - and options_path.get(field).get("200").get("headers") - ): - headers = definition_body["paths"][path]["options"][field]["200"]["headers"] - for header in headers.keys(): - header_value = { - "schema": definition_body["paths"][path]["options"][field]["200"][ - "headers" - ][header] - } + SwaggerEditor.validate_is_dict( + field_val, + "Value of responses in options method for path {} must be a " + "dictionary according to Swagger spec.".format(path), + ) + if field_val.get("200") and field_val.get("200").get("headers"): + headers = field_val["200"]["headers"] + for header, header_val in headers.items(): + new_header_val_with_schema = {"schema": header_val} definition_body["paths"][path]["options"][field]["200"]["headers"][ header - ] = header_value + ] = new_header_val_with_schema return definition_body diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 5718c5d0f..90d46cafc 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -139,16 +139,7 @@ def add_path(self, path, method=None): method = self._normalize_method_name(method) path_dict = self.paths.setdefault(path, {}) - - if not isinstance(path_dict, dict): - # Either customers has provided us an invalid Swagger, or this class has messed it somehow - raise InvalidDocumentException( - [ - InvalidTemplateException( - "Value of '{}' path must be a dictionary according to Swagger spec.".format(path) - ) - ] - ) + SwaggerEditor.validate_path_item_is_dict(path_dict, path) if self._CONDITIONAL_IF in path_dict: path_dict = path_dict[self._CONDITIONAL_IF][1] @@ -536,15 +527,10 @@ def set_path_default_authorizer( # It is possible that the method could have two definitions in a Fn::If block. for method_definition in self.get_method_contents(method): + SwaggerEditor.validate_is_dict( + method_definition, "{} for path {} is not a valid dictionary.".format(method_definition, path) + ) # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not isinstance(method_definition, dict): - raise InvalidDocumentException( - [ - InvalidTemplateException( - "{} for path {} is not a valid dictionary.".format(method_definition, path) - ) - ] - ) if not self.method_definition_has_integration(method_definition): continue existing_security = method_definition.get("security", []) @@ -559,14 +545,9 @@ def set_path_default_authorizer( # (e.g. sigv4 (AWS_IAM), api_key (API Key/Usage Plans), NONE (marker for ignoring default)) # We want to ensure only a single Authorizer security entry exists while keeping everything else for security in existing_security: - if not isinstance(security, dict): - raise InvalidDocumentException( - [ - InvalidTemplateException( - "{} in Security for path {} is not a valid dictionary.".format(security, path) - ) - ] - ) + SwaggerEditor.validate_is_dict( + security, "{} in Security for path {} is not a valid dictionary.".format(security, path) + ) if authorizer_names.isdisjoint(security.keys()): existing_non_authorizer_security.append(security) else: @@ -912,8 +893,7 @@ def add_resource_policy(self, resource_policy, path, stage): """ if resource_policy is None: return - if not isinstance(resource_policy, dict): - raise InvalidDocumentException([InvalidTemplateException("Resource Policy is not a valid dictionary.")]) + SwaggerEditor.validate_is_dict(resource_policy, "Resource Policy is not a valid dictionary.") aws_account_whitelist = resource_policy.get("AwsAccountWhitelist") aws_account_blacklist = resource_policy.get("AwsAccountBlacklist") @@ -1244,6 +1224,31 @@ def is_valid(data): ) return False + @staticmethod + def validate_is_dict(obj, exception_message): + """ + Throws exception if obj is not a dict + + :param obj: object being validated + :param exception_message: message to include in exception if obj is not a dict + """ + + if not isinstance(obj, dict): + raise InvalidDocumentException([InvalidTemplateException(exception_message)]) + + @staticmethod + def validate_path_item_is_dict(path_item, path): + """ + Throws exception if path_item is not a dict + + :param path_item: path_item (value at the path) being validated + :param path: path name + """ + + SwaggerEditor.validate_is_dict( + path_item, "Value of '{}' path must be a dictionary according to Swagger spec.".format(path) + ) + @staticmethod def gen_skeleton(): """ diff --git a/tests/translator/input/error_api_no_cors_invalid_openapi_null_path.yaml b/tests/translator/input/error_api_no_cors_invalid_openapi_null_path.yaml new file mode 100644 index 000000000..571b87ef9 --- /dev/null +++ b/tests/translator/input/error_api_no_cors_invalid_openapi_null_path.yaml @@ -0,0 +1,12 @@ +Resources: + ApiWithInvalidPath: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + OpenApiVersion: 3.0.1 + DefinitionBody: + openapi: 3.0.1 + info: + title: test invalid paths Api + paths: + /foo: null \ No newline at end of file diff --git a/tests/translator/input/error_api_no_cors_invalid_openapi_string_path.yaml b/tests/translator/input/error_api_no_cors_invalid_openapi_string_path.yaml new file mode 100644 index 000000000..745037fec --- /dev/null +++ b/tests/translator/input/error_api_no_cors_invalid_openapi_string_path.yaml @@ -0,0 +1,12 @@ +Resources: + ApiWithInvalidPath: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + OpenApiVersion: 3.0.1 + DefinitionBody: + openapi: 3.0.1 + info: + title: test invalid paths Api + paths: + /foo: invalid \ No newline at end of file diff --git a/tests/translator/output/error_api_no_cors_invalid_openapi_null_path.json b/tests/translator/output/error_api_no_cors_invalid_openapi_null_path.json new file mode 100644 index 000000000..2aedc0684 --- /dev/null +++ b/tests/translator/output/error_api_no_cors_invalid_openapi_null_path.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of '/foo' path must be a dictionary according to Swagger spec." +} \ No newline at end of file diff --git a/tests/translator/output/error_api_no_cors_invalid_openapi_string_path.json b/tests/translator/output/error_api_no_cors_invalid_openapi_string_path.json new file mode 100644 index 000000000..2aedc0684 --- /dev/null +++ b/tests/translator/output/error_api_no_cors_invalid_openapi_string_path.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of '/foo' path must be a dictionary according to Swagger spec." +} \ No newline at end of file From b45aef289e7bee85db25f56404bfa43a4df5d41d Mon Sep 17 00:00:00 2001 From: _sam <3804518+aahung@users.noreply.github.com> Date: Mon, 15 Nov 2021 14:00:01 -0800 Subject: [PATCH 07/59] Run unit tests in parallel (#2222) --- Makefile | 2 +- requirements/dev.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9675b8ccb..b2b7fdd36 100755 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ init: pip install -e '.[dev]' test: - pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 tests/* + pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 -n auto tests/* test-cov-report: pytest --cov samtranslator --cov-report term-missing --cov-report html --cov-fail-under 95 tests/* diff --git a/requirements/dev.txt b/requirements/dev.txt index 1424f80d6..ee58bcfa2 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,6 +2,7 @@ coverage~=5.3 flake8~=3.8.4 tox~=3.20.1 pytest-cov~=2.10.1 +pytest-xdist~=1.34.0 # pytest-xdist 2 is not compatible with Python 2.7 pylint>=1.7.2,<2.0 pyyaml~=5.4 From 78d641b6549fd78ad2048fe3ce7a1edcdb23a3ef Mon Sep 17 00:00:00 2001 From: _sam <3804518+aahung@users.noreply.github.com> Date: Mon, 15 Nov 2021 14:05:54 -0800 Subject: [PATCH 08/59] Handle when Api resource doesn't have properties (#2221) --- samtranslator/model/eventsources/push.py | 2 +- .../input/api_with_no_properties.yaml | 17 +++ .../output/api_with_no_properties.json | 124 ++++++++++++++++++ .../output/aws-cn/api_with_no_properties.json | 124 ++++++++++++++++++ .../aws-us-gov/api_with_no_properties.json | 124 ++++++++++++++++++ tests/translator/test_translator.py | 1 + 6 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 tests/translator/input/api_with_no_properties.yaml create mode 100644 tests/translator/output/api_with_no_properties.json create mode 100644 tests/translator/output/aws-cn/api_with_no_properties.json create mode 100644 tests/translator/output/aws-us-gov/api_with_no_properties.json diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 756c7cc78..4ded30218 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -1069,7 +1069,7 @@ def resources_to_link(self, resources): if isinstance(api_id, dict) and "Ref" in api_id: api_id = api_id["Ref"] - explicit_api = resources[api_id].get("Properties") + explicit_api = resources[api_id].get("Properties", {}) return {"explicit_api": explicit_api} diff --git a/tests/translator/input/api_with_no_properties.yaml b/tests/translator/input/api_with_no_properties.yaml new file mode 100644 index 000000000..5a963a3e9 --- /dev/null +++ b/tests/translator/input/api_with_no_properties.yaml @@ -0,0 +1,17 @@ +Resources: + HtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs12.x + Events: + GetHtml: + Type: HttpApi + Properties: + ApiId: HTTPApi + Path: / + Method: get + + HTTPApi: + Type: AWS::Serverless::HttpApi diff --git a/tests/translator/output/api_with_no_properties.json b/tests/translator/output/api_with_no_properties.json new file mode 100644 index 000000000..f609f642e --- /dev/null +++ b/tests/translator/output/api_with_no_properties.json @@ -0,0 +1,124 @@ +{ + "Resources": { + "HtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "HtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HtmlFunctionGetHtmlPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HtmlFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__ApiId__": "HTTPApi", + "__Stage__": "*" + } + ] + } + } + }, + "HTTPApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "openapi": "3.0.1", + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "get": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + }, + "HTTPApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HTTPApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + } + } +} diff --git a/tests/translator/output/aws-cn/api_with_no_properties.json b/tests/translator/output/aws-cn/api_with_no_properties.json new file mode 100644 index 000000000..5eaf3b3d2 --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_no_properties.json @@ -0,0 +1,124 @@ +{ + "Resources": { + "HtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "HtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HtmlFunctionGetHtmlPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HtmlFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__ApiId__": "HTTPApi", + "__Stage__": "*" + } + ] + } + } + }, + "HTTPApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "openapi": "3.0.1", + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "get": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + }, + "HTTPApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HTTPApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/api_with_no_properties.json b/tests/translator/output/aws-us-gov/api_with_no_properties.json new file mode 100644 index 000000000..9d09d1923 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_no_properties.json @@ -0,0 +1,124 @@ +{ + "Resources": { + "HtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "HtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HtmlFunctionGetHtmlPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HtmlFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__ApiId__": "HTTPApi", + "__Stage__": "*" + } + ] + } + } + }, + "HTTPApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "openapi": "3.0.1", + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "get": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + }, + "HTTPApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HTTPApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + } + } +} diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 1cf5673ba..0cb5d396f 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -327,6 +327,7 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): "api_request_model_with_validator", "api_with_stage_tags", "api_with_mode", + "api_with_no_properties", "s3", "s3_create_remove", "s3_existing_lambda_notification_configuration", From 2fd3beca6cbf9c52175967e25aa1f47422a1011d Mon Sep 17 00:00:00 2001 From: Jacob Fuss <32497805+jfuss@users.noreply.github.com> Date: Fri, 19 Nov 2021 10:01:25 -0600 Subject: [PATCH 09/59] Chore: Only add labels on open of PRs (#2225) --- .github/workflows/pr-labeler.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index c6066bd7a..e4d50db9d 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -1,6 +1,7 @@ name: "Pull Request Labeler" on: -- pull_request_target + pull_request_target: + types: [opened] jobs: apply-internal-external-label: From 038f2083f106d277b744fcd68c125e40b4fa03d7 Mon Sep 17 00:00:00 2001 From: _sam <3804518+aahung@users.noreply.github.com> Date: Mon, 22 Nov 2021 13:47:26 -0800 Subject: [PATCH 10/59] Handle when function http event auth or DefaultAuthorizer is not a string (#2234) --- samtranslator/model/eventsources/push.py | 7 +++++ .../input/error_api_invalid_auth.yaml | 29 ++++++++++++++++- .../input/error_http_api_invalid_auth.yaml | 31 ++++++++++++++++++- .../output/error_api_invalid_auth.json | 2 +- .../output/error_http_api_invalid_auth.json | 2 +- 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 4ded30218..ff444a6cc 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -1207,6 +1207,13 @@ def _add_auth_to_openapi_integration(self, api, editor): :param editor: OpenApiEditor object that contains the OpenApi definition """ method_authorizer = self.Auth.get("Authorizer") + + if method_authorizer is not None and not isinstance(method_authorizer, string_types): + raise InvalidEventException( + self.relative_id, + "'Authorizer' in the 'Auth' section must be a string.", + ) + api_auth = api.get("Auth", {}) if not method_authorizer: if api_auth.get("DefaultAuthorizer"): diff --git a/tests/translator/input/error_api_invalid_auth.yaml b/tests/translator/input/error_api_invalid_auth.yaml index c200c58f3..60d2b3f9d 100644 --- a/tests/translator/input/error_api_invalid_auth.yaml +++ b/tests/translator/input/error_api_invalid_auth.yaml @@ -193,4 +193,31 @@ Resources: StageName: Prod Auth: Authorizers: - MyCognitoAuthorizer: \ No newline at end of file + MyCognitoAuthorizer: + + NonStringDefaultAuthorizerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs12.x + Events: + GetRoot: + Type: HttpApi + Properties: + ApiId: NonStringDefaultAuthorizerApi + Path: / + Method: get + + NonStringDefaultAuthorizerApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + Authorizers: + MyAuth: + JwtConfiguration: + audience: https://test-sam.com + issuer: https://test-sam.com + IdentitySource: "$request.header.Authorization" + # Correct usage: DefaultAuthorizer: MyAuth + DefaultAuthorizer: !Ref MyAuth diff --git a/tests/translator/input/error_http_api_invalid_auth.yaml b/tests/translator/input/error_http_api_invalid_auth.yaml index a45f74800..ce98d019b 100644 --- a/tests/translator/input/error_http_api_invalid_auth.yaml +++ b/tests/translator/input/error_http_api_invalid_auth.yaml @@ -67,6 +67,24 @@ Resources: Auth: Authorizer: OIDC + NonStringAuthFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: index.handler + Runtime: nodejs12.x + Events: + GetRoot: + Type: HttpApi + Properties: + ApiId: NonStringAuthFunctionApi + Path: / + Method: get + Auth: + # Correct usage: Authorizer: MyAuth + Authorizer: !Ref MyAuth + InvokeRole: CALLER_CREDENTIALS + MyApi: Type: AWS::Serverless::HttpApi Properties: @@ -162,4 +180,15 @@ Resources: title: Ref: AWS::StackName paths: {} - openapi: 3.0.1 \ No newline at end of file + openapi: 3.0.1 + + NonStringAuthFunctionApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + Authorizers: + MyAuth: + JwtConfiguration: + audience: https://test-sam.com + issuer: https://test-sam.com + IdentitySource: "$request.header.Authorization" diff --git a/tests/translator/output/error_api_invalid_auth.json b/tests/translator/output/error_api_invalid_auth.json index 6cdd59dbe..323721f3b 100644 --- a/tests/translator/output/error_api_invalid_auth.json +++ b/tests/translator/output/error_api_invalid_auth.json @@ -1,3 +1,3 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 15. Resource with id [AuthNotDictApi] is invalid. Type of property 'Auth' is invalid. Resource with id [AuthWithAdditionalPropertyApi] is invalid. Invalid value for 'Auth' property Resource with id [AuthWithDefinitionUriApi] is invalid. Auth works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [AuthWithInvalidDefinitionBodyApi] is invalid. Unable to add Auth configuration because 'DefinitionBody' does not contain a valid Swagger definition. Resource with id [AuthWithMissingDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because 'NotThere' was not defined in 'Authorizers'. Resource with id [AuthorizerNotDict] is invalid. Authorizer MyCognitoAuthorizer must be a dictionary. Resource with id [AuthorizersNotDictApi] is invalid. Authorizers must be a dictionary. Resource with id [InvalidFunctionPayloadTypeApi] is invalid. MyLambdaAuthorizer Authorizer has invalid 'FunctionPayloadType': INVALID. Resource with id [MissingAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [UnspecifiedAuthorizer] on API method [get] for path [/] because it wasn't defined in the API's Authorizers. Resource with id [NoApiAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthorizersFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoDefaultAuthorizerWithNoneFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer on API method [get] for path [/] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [NoIdentityOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NoIdentitySourceOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 16. Resource with id [AuthNotDictApi] is invalid. Type of property 'Auth' is invalid. Resource with id [AuthWithAdditionalPropertyApi] is invalid. Invalid value for 'Auth' property Resource with id [AuthWithDefinitionUriApi] is invalid. Auth works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [AuthWithInvalidDefinitionBodyApi] is invalid. Unable to add Auth configuration because 'DefinitionBody' does not contain a valid Swagger definition. Resource with id [AuthWithMissingDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because 'NotThere' was not defined in 'Authorizers'. Resource with id [AuthorizerNotDict] is invalid. Authorizer MyCognitoAuthorizer must be a dictionary. Resource with id [AuthorizersNotDictApi] is invalid. Authorizers must be a dictionary. Resource with id [InvalidFunctionPayloadTypeApi] is invalid. MyLambdaAuthorizer Authorizer has invalid 'FunctionPayloadType': INVALID. Resource with id [MissingAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [UnspecifiedAuthorizer] on API method [get] for path [/] because it wasn't defined in the API's Authorizers. Resource with id [NoApiAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthorizersFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoDefaultAuthorizerWithNoneFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer on API method [get] for path [/] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [NoIdentityOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NoIdentitySourceOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NonStringDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because intrinsic functions are not supported for this field." } diff --git a/tests/translator/output/error_http_api_invalid_auth.json b/tests/translator/output/error_http_api_invalid_auth.json index e5bdcb426..e97089a78 100644 --- a/tests/translator/output/error_http_api_invalid_auth.json +++ b/tests/translator/output/error_http_api_invalid_auth.json @@ -4,5 +4,5 @@ "errorMessage": "Resource with id [Function] is invalid. Event with id [Api] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because the related API does not define any Authorizers. Resource with id [Function2] is invalid. Event with id [Api2] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because it wasn't defined in the API's Authorizers. Resource with id [Function3] is invalid. Event with id [Api3] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [Function4] is invalid. Event with id [Api4] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'AuthorizationScopes' must be a list of strings." } ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 5. Resource with id [Function] is invalid. Event with id [Api] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because the related API does not define any Authorizers. Resource with id [Function2] is invalid. Event with id [Api2] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because it wasn't defined in the API's Authorizers. Resource with id [Function3] is invalid. Event with id [Api3] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [Function4] is invalid. Event with id [Api4] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'AuthorizationScopes' must be a list of strings. Resource with id [MyApi5] is invalid. 'OpenIdConnectUrl' is no longer a supported property for authorizer 'OIDC'. Please refer to the AWS SAM documentation." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [Function] is invalid. Event with id [Api] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because the related API does not define any Authorizers. Resource with id [Function2] is invalid. Event with id [Api2] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because it wasn't defined in the API's Authorizers. Resource with id [Function3] is invalid. Event with id [Api3] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [Function4] is invalid. Event with id [Api4] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'AuthorizationScopes' must be a list of strings. Resource with id [MyApi5] is invalid. 'OpenIdConnectUrl' is no longer a supported property for authorizer 'OIDC'. Please refer to the AWS SAM documentation. Resource with id [NonStringAuthFunction] is invalid. Event with id [GetRoot] is invalid. 'Authorizer' in the 'Auth' section must be a string." } \ No newline at end of file From e816e50260dde43c8c95e7302b964064254bec4c Mon Sep 17 00:00:00 2001 From: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Date: Mon, 22 Nov 2021 17:23:08 -0800 Subject: [PATCH 11/59] chore: bump version to 1.42.0 (#2235) --- samtranslator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index c4a649e59..5d0a428bc 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.41.0" +__version__ = "1.42.0" From c7da9d4e2266560b9fbf7a30c41f0afd5a574c5d Mon Sep 17 00:00:00 2001 From: Wilton_ Date: Tue, 23 Nov 2021 11:24:57 -0800 Subject: [PATCH 12/59] Revert "chore: bump version to 1.42.0 (#2235)" (#2237) This reverts commit e816e50260dde43c8c95e7302b964064254bec4c. --- samtranslator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index 5d0a428bc..c4a649e59 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.42.0" +__version__ = "1.41.0" From 65048488b6c45966d30958c8ab9bb8f270946e23 Mon Sep 17 00:00:00 2001 From: _sam <3804518+aahung@users.noreply.github.com> Date: Mon, 29 Nov 2021 13:33:21 -0800 Subject: [PATCH 13/59] Validate swagger path item objects (#2251) --- samtranslator/swagger/swagger.py | 8 ++++ tests/swagger/test_swagger.py | 41 ++++++++----------- .../error_api_auth_invalid_path_item.yaml | 28 +++++++++++++ .../input/error_api_auth_null_path_item.yaml | 28 +++++++++++++ .../error_api_auth_invalid_path_item.json | 1 + .../output/error_api_auth_null_path_item.json | 1 + 6 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 tests/translator/input/error_api_auth_invalid_path_item.yaml create mode 100644 tests/translator/input/error_api_auth_null_path_item.yaml create mode 100644 tests/translator/output/error_api_auth_invalid_path_item.json create mode 100644 tests/translator/output/error_api_auth_null_path_item.json diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 41984b7e9..34cd786c8 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -53,6 +53,14 @@ def __init__(self, doc): self.resource_policy = self._doc.get(self._X_APIGW_POLICY, {}) self.definitions = self._doc.get("definitions", {}) + # https://swagger.io/specification/#path-item-object + # According to swagger spec, + # each path item object must be a dict (even it is empty). + # We can do an early path validation on path item objects, + # so we don't need to validate wherever we use them. + for path in self.iter_on_path(): + SwaggerEditor.validate_path_item_is_dict(self.get_path(path), path) + def get_path(self, path): path_dict = self.paths.get(path) if isinstance(path_dict, dict) and self._CONDITIONAL_IF in path_dict: diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index ec33f7d86..0a50d6a7b 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -49,6 +49,13 @@ def test_must_succeed_on_valid_openapi3(self): self.assertEqual(editor.paths, {"/foo": {}, "/bar": {}}) + @parameterized.expand([(None,), ("should-not-be-string",)]) + def test_must_fail_with_bad_values_for_path(self, invalid_path_item): + invalid_swagger = {"openapi": "3.1.1.1", "paths": {"/foo": {}, "/bad": invalid_path_item}} + + with self.assertRaises(ValueError): + SwaggerEditor(invalid_swagger) + class TestSwaggerEditor_has_path(TestCase): def setUp(self): @@ -57,7 +64,6 @@ def setUp(self): "paths": { "/foo": {"get": {}, "somemethod": {}}, "/bar": {"post": {}, _X_ANY_METHOD: {}}, - "badpath": "string value", }, } @@ -97,11 +103,6 @@ def test_must_not_find_path_and_method(self): self.assertFalse(self.editor.has_path("/bar", "get")) self.assertFalse(self.editor.has_path("/bar", "xyz")) - def test_must_not_fail_on_bad_path(self): - - self.assertTrue(self.editor.has_path("badpath")) - self.assertFalse(self.editor.has_path("badpath", "somemethod")) - class TestSwaggerEditor_has_integration(TestCase): def setUp(self): @@ -145,7 +146,10 @@ def setUp(self): self.original_swagger = { "swagger": "2.0", - "paths": {"/foo": {"get": {"a": "b"}}, "/bar": {}, "/badpath": "string value"}, + "paths": { + "/foo": {"get": {"a": "b"}}, + "/bar": {}, + }, } self.editor = SwaggerEditor(self.original_swagger) @@ -165,14 +169,6 @@ def test_must_add_new_path_and_method(self, path, method, case): self.assertTrue(self.editor.has_path(path, method), "must add for " + case) self.assertEqual(self.editor.swagger["paths"][path][method], {}) - def test_must_raise_non_dict_path_values(self): - - path = "/badpath" - method = "get" - - with self.assertRaises(InvalidDocumentException): - self.editor.add_path(path, method) - def test_must_skip_existing_path(self): """ Given an existing path/method, this must @@ -295,13 +291,13 @@ def test_must_add_credentials_to_the_integration_overrides(self): class TestSwaggerEditor_iter_on_path(TestCase): def setUp(self): - self.original_swagger = {"swagger": "2.0", "paths": {"/foo": {}, "/bar": {}, "/baz": "some value"}} + self.original_swagger = {"swagger": "2.0", "paths": {"/foo": {}, "/bar": {}}} self.editor = SwaggerEditor(self.original_swagger) def test_must_iterate_on_paths(self): - expected = {"/foo", "/bar", "/baz"} + expected = {"/foo", "/bar"} actual = set(list(self.editor.iter_on_path())) self.assertEqual(expected, actual) @@ -312,7 +308,10 @@ def setUp(self): self.original_swagger = { "swagger": "2.0", - "paths": {"/foo": {}, "/withoptions": {"options": {"some": "value"}}, "/bad": "some value"}, + "paths": { + "/foo": {}, + "/withoptions": {"options": {"some": "value"}}, + }, } self.editor = SwaggerEditor(self.original_swagger) @@ -343,12 +342,6 @@ def test_must_skip_existing_path(self): self.editor.add_cors(path, "origins", "headers", "methods") self.assertEqual(expected, self.editor.swagger["paths"][path]["options"]) - def test_must_fail_with_bad_values_for_path(self): - path = "/bad" - - with self.assertRaises(InvalidDocumentException): - self.editor.add_cors(path, "origins", "headers", "methods") - def test_must_fail_for_invalid_allowed_origin(self): path = "/foo" diff --git a/tests/translator/input/error_api_auth_invalid_path_item.yaml b/tests/translator/input/error_api_auth_invalid_path_item.yaml new file mode 100644 index 000000000..9967eab57 --- /dev/null +++ b/tests/translator/input/error_api_auth_invalid_path_item.yaml @@ -0,0 +1,28 @@ +Resources: + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: SomeStage + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + DefinitionBody: + swagger: 2.0 + paths: + "/": null + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false diff --git a/tests/translator/input/error_api_auth_null_path_item.yaml b/tests/translator/input/error_api_auth_null_path_item.yaml new file mode 100644 index 000000000..ca2cd3aa9 --- /dev/null +++ b/tests/translator/input/error_api_auth_null_path_item.yaml @@ -0,0 +1,28 @@ +Resources: + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: SomeStage + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + DefinitionBody: + swagger: 2.0 + paths: + "/": "" + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false diff --git a/tests/translator/output/error_api_auth_invalid_path_item.json b/tests/translator/output/error_api_auth_invalid_path_item.json new file mode 100644 index 000000000..2d9c18093 --- /dev/null +++ b/tests/translator/output/error_api_auth_invalid_path_item.json @@ -0,0 +1 @@ + {"errorMessage":"Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of '/' path must be a dictionary according to Swagger spec."} diff --git a/tests/translator/output/error_api_auth_null_path_item.json b/tests/translator/output/error_api_auth_null_path_item.json new file mode 100644 index 000000000..2d9c18093 --- /dev/null +++ b/tests/translator/output/error_api_auth_null_path_item.json @@ -0,0 +1 @@ + {"errorMessage":"Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of '/' path must be a dictionary according to Swagger spec."} From c8057b022cc55f3268c7b10b3a1fca26d12be13f Mon Sep 17 00:00:00 2001 From: _sam <3804518+aahung@users.noreply.github.com> Date: Mon, 29 Nov 2021 13:40:11 -0800 Subject: [PATCH 14/59] Validate swagger method value is a valid dict before processing (#2250) --- samtranslator/swagger/swagger.py | 10 +++++-- .../input/error_null_method_definition.yaml | 29 +++++++++++++++++++ .../error_invalid_method_definition.json | 2 +- .../output/error_null_method_definition.json | 1 + 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 tests/translator/input/error_null_method_definition.yaml create mode 100644 tests/translator/output/error_null_method_definition.json diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 34cd786c8..3151095ed 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -545,12 +545,18 @@ def set_path_default_authorizer( if normalized_method_name in SwaggerEditor._EXCLUDED_PATHS_FIELDS: continue if add_default_auth_to_preflight or normalized_method_name != "options": - normalized_method_name = self._normalize_method_name(method_name) + SwaggerEditor.validate_is_dict( + method, + 'Value of "{}" ({}) for path {} is not a valid dictionary.'.format(method_name, method, path), + ) # It is possible that the method could have two definitions in a Fn::If block. for method_definition in self.get_method_contents(method): SwaggerEditor.validate_is_dict( - method_definition, "{} for path {} is not a valid dictionary.".format(method_definition, path) + method_definition, + 'Value of "{}" ({}) for path {} is not a valid dictionary.'.format( + method_name, method_definition, path + ), ) # If no integration given, then we don't need to process this definition (could be AWS::NoValue) if not self.method_definition_has_integration(method_definition): diff --git a/tests/translator/input/error_null_method_definition.yaml b/tests/translator/input/error_null_method_definition.yaml new file mode 100644 index 000000000..4462d8ebc --- /dev/null +++ b/tests/translator/input/error_null_method_definition.yaml @@ -0,0 +1,29 @@ +Resources: + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: SomeStage + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + DefinitionBody: + swagger: 2.0 + paths: + "/": + get: null + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false diff --git a/tests/translator/output/error_invalid_method_definition.json b/tests/translator/output/error_invalid_method_definition.json index 01ff1502c..25c4f8cc1 100644 --- a/tests/translator/output/error_invalid_method_definition.json +++ b/tests/translator/output/error_invalid_method_definition.json @@ -1 +1 @@ -{"errorMessage":"Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. ['InvalidMethodDefinition'] for path / is not a valid dictionary."} \ No newline at end of file +{"errorMessage":"Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of \"tags\" (['InvalidMethodDefinition']) for path / is not a valid dictionary."} \ No newline at end of file diff --git a/tests/translator/output/error_null_method_definition.json b/tests/translator/output/error_null_method_definition.json new file mode 100644 index 000000000..ae7f7eadc --- /dev/null +++ b/tests/translator/output/error_null_method_definition.json @@ -0,0 +1 @@ +{"errorMessage":"Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Value of \"get\" (None) for path / is not a valid dictionary."} \ No newline at end of file From 8ca5bdda15355c0ea7d8620cce679ae529477594 Mon Sep 17 00:00:00 2001 From: Ruperto Torres <86501267+torresxb1@users.noreply.github.com> Date: Mon, 29 Nov 2021 13:52:37 -0800 Subject: [PATCH 15/59] fix: fix validation errors log message that calls join on a string (#2245) --- samtranslator/parser/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/parser/parser.py b/samtranslator/parser/parser.py index 9c0e0b13d..b060cc028 100644 --- a/samtranslator/parser/parser.py +++ b/samtranslator/parser/parser.py @@ -63,7 +63,7 @@ def _validate(self, sam_template, parameter_values): validation_errors = validator.validate(sam_template) if validation_errors: - LOG.warn("Template schema validation reported the following errors: " + ", ".join(validation_errors)) + LOG.warning("Template schema validation reported the following errors: %s", validation_errors) except Exception as e: # Catching any exception and not re-raising to make sure any validation process won't break transform LOG.exception("Exception from SamTemplateValidator: %s", e) From d2cf4b7e23d26cdf677c564d53bb58e6a5b6cac2 Mon Sep 17 00:00:00 2001 From: mingkun2020 <68391979+mingkun2020@users.noreply.github.com> Date: Thu, 2 Dec 2021 16:23:14 -0800 Subject: [PATCH 16/59] [FOSS] hot fix and improvement (#2207) hotfix for foss integration tests --- .../combination/test_api_with_authorizers.py | 5 + .../test_api_with_gateway_responses.py | 7 + .../combination/test_api_with_usage_plan.py | 5 + .../combination/test_function_with_alias.py | 7 +- .../test_function_with_all_event_types.py | 14 +- .../test_function_with_application.py | 24 +++- ..._function_with_cwe_dlq_and_retry_policy.py | 5 + .../test_function_with_cwe_dlq_generated.py | 5 +- ...est_function_with_deployment_preference.py | 9 +- .../test_function_with_dynamoDB.py | 5 + .../test_function_with_http_api.py | 5 + .../test_function_with_implicit_http_api.py | 5 + .../combination/test_function_with_kinesis.py | 5 + .../combination/test_function_with_layers.py | 5 + .../combination/test_function_with_mq.py | 37 ++++- .../combination/test_function_with_msk.py | 33 ++++- .../test_function_with_schedule.py | 11 +- ...tion_with_schedule_dlq_and_retry_policy.py | 5 + ...st_function_with_schedule_dlq_generated.py | 5 + .../test_function_with_signing_profile.py | 7 +- .../combination/test_function_with_sns.py | 5 + .../combination/test_function_with_sqs.py | 9 +- .../test_function_with_user_pool_event.py | 5 + .../combination/test_http_api_with_auth.py | 5 + .../combination/test_http_api_with_cors.py | 5 + ...p_api_with_disable_execute_api_endpoint.py | 5 + ...e_machine_with_cwe_dlq_and_retry_policy.py | 5 + ...st_state_machine_with_cwe_dlq_generated.py | 5 + ...est_state_machine_with_policy_templates.py | 5 + ...hine_with_schedule_dlq_and_retry_policy.py | 5 + ...ate_machine_with_schedule_dlq_generated.py | 5 + .../config/region_service_exclusion.yaml | 5 + integration/config/service_names.py | 22 +++ integration/conftest.py | 130 ++++++++++++++++++ integration/helpers/base_test.py | 78 ++++++++--- integration/helpers/deployer/deployer.py | 24 +++- .../helpers/deployer/exceptions/exceptions.py | 10 ++ integration/helpers/deployer/utils/retry.py | 36 +++++ integration/helpers/stack.py | 75 ++++++++++ .../metrics/test_metrics_integration.py | 2 +- .../combination/function_with_mq.json | 4 - .../function_with_mq_using_autogen_role.json | 4 - .../combination/function_with_msk.json | 3 - ...unction_with_msk_using_managed_policy.json | 3 - .../expected/single/basic_api_with_mode.json | 2 +- .../combination/all_policy_templates.yaml | 6 +- .../templates/combination/depends_on.yaml | 2 +- .../function_with_all_event_types.yaml | 27 +--- .../function_with_custom_code_deploy.yaml | 2 +- .../function_with_deployment_basic.yaml | 2 +- ...eployment_default_role_managed_policy.yaml | 2 +- .../function_with_deployment_globals.yaml | 2 +- .../combination/function_with_mq.yaml | 57 +++----- .../function_with_mq_using_autogen_role.yaml | 54 +++----- .../combination/function_with_msk.yaml | 61 ++------ ...unction_with_msk_using_managed_policy.yaml | 57 ++------ .../function_with_policy_templates.yaml | 2 +- .../function_with_resource_refs.yaml | 4 +- .../combination/function_with_schedule.yaml | 25 +--- .../state_machine_with_policy_templates.yaml | 2 +- .../basic_application_sar_location.yaml | 2 +- ...lication_sar_location_with_intrinsics.yaml | 2 +- .../basic_function_event_destinations.yaml | 6 +- .../single/basic_layer_with_parameters.yaml | 2 +- ...oyment_preference_alarms_intrinsic_if.yaml | 2 +- integration/setup/__init__.py | 0 integration/setup/companion-stack.yaml | 60 ++++++++ integration/setup/test_setup_teardown.py | 11 ++ integration/single/test_basic_api.py | 17 ++- integration/single/test_basic_application.py | 4 +- 70 files changed, 786 insertions(+), 286 deletions(-) create mode 100644 integration/config/service_names.py create mode 100644 integration/conftest.py create mode 100644 integration/helpers/stack.py create mode 100644 integration/setup/__init__.py create mode 100644 integration/setup/companion-stack.yaml create mode 100644 integration/setup/test_setup_teardown.py diff --git a/integration/combination/test_api_with_authorizers.py b/integration/combination/test_api_with_authorizers.py index d08a27a47..4d6975d64 100644 --- a/integration/combination/test_api_with_authorizers.py +++ b/integration/combination/test_api_with_authorizers.py @@ -1,10 +1,15 @@ +from unittest.case import skipIf + import requests from integration.helpers.base_test import BaseTest from integration.helpers.deployer.utils.retry import retry from integration.helpers.exception import StatusCodeError +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import COGNITO +@skipIf(current_region_does_not_support([COGNITO]), "Cognito is not supported in this testing region") class TestApiWithAuthorizers(BaseTest): def test_authorizers_min(self): self.create_and_verify_stack("combination/api_with_authorizers_min") diff --git a/integration/combination/test_api_with_gateway_responses.py b/integration/combination/test_api_with_gateway_responses.py index 1ff2efc76..3d77a51b3 100644 --- a/integration/combination/test_api_with_gateway_responses.py +++ b/integration/combination/test_api_with_gateway_responses.py @@ -1,6 +1,13 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import GATEWAY_RESPONSES +@skipIf( + current_region_does_not_support([GATEWAY_RESPONSES]), "GatewayResponses is not supported in this testing region" +) class TestApiWithGatewayResponses(BaseTest): def test_gateway_responses(self): self.create_and_verify_stack("combination/api_with_gateway_responses") diff --git a/integration/combination/test_api_with_usage_plan.py b/integration/combination/test_api_with_usage_plan.py index d77b16a68..b252338a9 100644 --- a/integration/combination/test_api_with_usage_plan.py +++ b/integration/combination/test_api_with_usage_plan.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import USAGE_PLANS +@skipIf(current_region_does_not_support([USAGE_PLANS]), "UsagePlans is not supported in this testing region") class TestApiWithUsagePlan(BaseTest): def test_api_with_usage_plans(self): self.create_and_verify_stack("combination/api_with_usage_plan") diff --git a/integration/combination/test_function_with_alias.py b/integration/combination/test_function_with_alias.py index 8aab5a472..5eb8d01aa 100644 --- a/integration/combination/test_function_with_alias.py +++ b/integration/combination/test_function_with_alias.py @@ -71,12 +71,13 @@ def test_function_with_alias_with_intrinsics(self): # Let's change Key by updating the template parameter, but keep template same # This should create a new version and leave existing version intact parameters[1]["ParameterValue"] = "code2.zip" - self.deploy_stack(parameters) + # self.deploy_stack(parameters) + self.update_stack("combination/function_with_alias_intrinsics", parameters) version_ids = get_function_versions(function_name, self.client_provider.lambda_client) - self.assertEqual(["1", "2"], version_ids) + self.assertEqual(["1"], version_ids) alias = self.get_alias(function_name, alias_name) - self.assertEqual("2", alias["FunctionVersion"]) + self.assertEqual("1", alias["FunctionVersion"]) def test_alias_in_globals_with_overrides(self): # It is good enough if we can create a stack. Globals are pre-processed on the SAM template and don't diff --git a/integration/combination/test_function_with_all_event_types.py b/integration/combination/test_function_with_all_event_types.py index a8647003c..a29c0bd1e 100644 --- a/integration/combination/test_function_with_all_event_types.py +++ b/integration/combination/test_function_with_all_event_types.py @@ -1,11 +1,20 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support, generate_suffix +from integration.config.service_names import IOT, SCHEDULE_EVENT +@skipIf( + current_region_does_not_support([IOT, SCHEDULE_EVENT]), + "IoT, ScheduleEvent is not supported in this testing region", +) class TestFunctionWithAllEventTypes(BaseTest): def test_function_with_all_event_types(self): - self.create_and_verify_stack("combination/function_with_all_event_types") + schedule_name = "TestSchedule" + generate_suffix() + parameters = [self.generate_parameter("ScheduleName", schedule_name)] - stack_outputs = self.get_stack_outputs() + self.create_and_verify_stack("combination/function_with_all_event_types", parameters) # make sure bucket notification configurations are added s3_client = self.client_provider.s3_client @@ -30,7 +39,6 @@ def test_function_with_all_event_types(self): self.assertEqual(len(rule_names), 2) # make sure cloudwatch Schedule event has properties: name, state and description - schedule_name = stack_outputs["ScheduleName"] cw_rule_result = cloudwatch_events_client.describe_rule(Name=schedule_name) self.assertEqual(cw_rule_result["Name"], schedule_name) diff --git a/integration/combination/test_function_with_application.py b/integration/combination/test_function_with_application.py index e04219898..1e238d457 100644 --- a/integration/combination/test_function_with_application.py +++ b/integration/combination/test_function_with_application.py @@ -1,7 +1,18 @@ +from unittest.case import skipIf + +from botocore.exceptions import ClientError + from integration.helpers.base_test import BaseTest +from integration.helpers.deployer.exceptions.exceptions import ThrottlingError +from integration.helpers.deployer.utils.retry import retry_with_exponential_backoff_and_jitter +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import SERVERLESS_REPO class TestFunctionWithApplication(BaseTest): + @skipIf( + current_region_does_not_support([SERVERLESS_REPO]), "ServerlessRepo is not supported in this testing region" + ) def test_function_referencing_outputs_from_application(self): self.create_and_verify_stack("combination/function_with_application") @@ -11,7 +22,18 @@ def test_function_referencing_outputs_from_application(self): cfn_client = self.client_provider.cfn_client function_config = lambda_client.get_function_configuration(FunctionName=lambda_function_name) - stack_result = cfn_client.describe_stacks(StackName=nested_stack_name) + stack_result = self._describe_stacks(cfn_client, nested_stack_name) expected = stack_result["Stacks"][0]["Outputs"][0]["OutputValue"] self.assertEqual(function_config["Environment"]["Variables"]["TABLE_NAME"], expected) + + @retry_with_exponential_backoff_and_jitter(ThrottlingError, 5, 360) + def _describe_stacks(self, cfn_client, stack_name): + try: + stack_result = cfn_client.describe_stacks(StackName=stack_name) + except ClientError as ex: + if "Throttling" in str(ex): + raise ThrottlingError(stack_name=stack_name, msg=str(ex)) + raise ex + + return stack_result diff --git a/integration/combination/test_function_with_cwe_dlq_and_retry_policy.py b/integration/combination/test_function_with_cwe_dlq_and_retry_policy.py index 17ce96710..220d8963d 100644 --- a/integration/combination/test_function_with_cwe_dlq_and_retry_policy.py +++ b/integration/combination/test_function_with_cwe_dlq_and_retry_policy.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CWE_CWS_DLQ +@skipIf(current_region_does_not_support([CWE_CWS_DLQ]), "CweCwsDlq is not supported in this testing region") class TestFunctionWithCweDlqAndRetryPolicy(BaseTest): def test_function_with_cwe(self): # Verifying that following resources were created is correct diff --git a/integration/combination/test_function_with_cwe_dlq_generated.py b/integration/combination/test_function_with_cwe_dlq_generated.py index 92491fd84..138da7476 100644 --- a/integration/combination/test_function_with_cwe_dlq_generated.py +++ b/integration/combination/test_function_with_cwe_dlq_generated.py @@ -1,9 +1,12 @@ import json +from unittest.case import skipIf from integration.helpers.base_test import BaseTest -from integration.helpers.resource import first_item_in_dict +from integration.helpers.resource import first_item_in_dict, current_region_does_not_support +from integration.config.service_names import CWE_CWS_DLQ +@skipIf(current_region_does_not_support([CWE_CWS_DLQ]), "CweCwsDlq is not supported in this testing region") class TestFunctionWithCweDlqGenerated(BaseTest): def test_function_with_cwe(self): # Verifying that following resources were created is correct diff --git a/integration/combination/test_function_with_deployment_preference.py b/integration/combination/test_function_with_deployment_preference.py index b2b2461ee..56f0dd2aa 100644 --- a/integration/combination/test_function_with_deployment_preference.py +++ b/integration/combination/test_function_with_deployment_preference.py @@ -1,10 +1,15 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CODE_DEPLOY CODEDEPLOY_APPLICATION_LOGICAL_ID = "ServerlessDeploymentApplication" LAMBDA_FUNCTION_NAME = "MyLambdaFunction" LAMBDA_ALIAS = "Live" +@skipIf(current_region_does_not_support([CODE_DEPLOY]), "CodeDeploy is not supported in this testing region") class TestFunctionWithDeploymentPreference(BaseTest): def test_lambda_function_with_deployment_preference_uses_code_deploy(self): self.create_and_verify_stack("combination/function_with_deployment_basic") @@ -97,7 +102,9 @@ def _get_deployment_groups(self, application_name): ] def _get_deployments(self, application_name, deployment_group): - deployments = self.client_provider.code_deploy_client.list_deployments()["deployments"] + deployments = self.client_provider.code_deploy_client.list_deployments( + applicationName=application_name, deploymentGroupName=deployment_group + )["deployments"] deployment_infos = [self._get_deployment_info(deployment_id) for deployment_id in deployments] return deployment_infos diff --git a/integration/combination/test_function_with_dynamoDB.py b/integration/combination/test_function_with_dynamoDB.py index eb702679b..adf725b9b 100644 --- a/integration/combination/test_function_with_dynamoDB.py +++ b/integration/combination/test_function_with_dynamoDB.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import DYNAMO_DB +@skipIf(current_region_does_not_support([DYNAMO_DB]), "DynamoDB is not supported in this testing region") class TestFunctionWithDynamoDB(BaseTest): def test_function_with_dynamoDB_trigger(self): self.create_and_verify_stack("combination/function_with_dynamodb") diff --git a/integration/combination/test_function_with_http_api.py b/integration/combination/test_function_with_http_api.py index 139f3254a..337edc429 100644 --- a/integration/combination/test_function_with_http_api.py +++ b/integration/combination/test_function_with_http_api.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import HTTP_API +@skipIf(current_region_does_not_support([HTTP_API]), "HttpApi is not supported in this testing region") class TestFunctionWithHttpApi(BaseTest): def test_function_with_http_api(self): self.create_and_verify_stack("combination/function_with_http_api") diff --git a/integration/combination/test_function_with_implicit_http_api.py b/integration/combination/test_function_with_implicit_http_api.py index fdaa8ebb9..5803bc9e6 100644 --- a/integration/combination/test_function_with_implicit_http_api.py +++ b/integration/combination/test_function_with_implicit_http_api.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import HTTP_API +@skipIf(current_region_does_not_support([HTTP_API]), "HttpApi is not supported in this testing region") class TestFunctionWithImplicitHttpApi(BaseTest): def test_function_with_implicit_api(self): self.create_and_verify_stack("combination/function_with_implicit_http_api") diff --git a/integration/combination/test_function_with_kinesis.py b/integration/combination/test_function_with_kinesis.py index 2e5af72aa..425a657f0 100644 --- a/integration/combination/test_function_with_kinesis.py +++ b/integration/combination/test_function_with_kinesis.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import KINESIS +@skipIf(current_region_does_not_support([KINESIS]), "Kinesis is not supported in this testing region") class TestFunctionWithKinesis(BaseTest): def test_function_with_kinesis_trigger(self): self.create_and_verify_stack("combination/function_with_kinesis") diff --git a/integration/combination/test_function_with_layers.py b/integration/combination/test_function_with_layers.py index c75a509da..ee12fc7ea 100644 --- a/integration/combination/test_function_with_layers.py +++ b/integration/combination/test_function_with_layers.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import LAYERS +@skipIf(current_region_does_not_support([LAYERS]), "Layers is not supported in this testing region") class TestFunctionWithLayers(BaseTest): def test_function_with_layer(self): self.create_and_verify_stack("combination/function_with_layer") diff --git a/integration/combination/test_function_with_mq.py b/integration/combination/test_function_with_mq.py index a87bddd75..d63f5c1b7 100644 --- a/integration/combination/test_function_with_mq.py +++ b/integration/combination/test_function_with_mq.py @@ -1,17 +1,39 @@ +from unittest.case import skipIf + +import pytest from parameterized import parameterized from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support, generate_suffix +from integration.config.service_names import MQ +@skipIf(current_region_does_not_support([MQ]), "MQ is not supported in this testing region") class TestFunctionWithMq(BaseTest): + @pytest.fixture(autouse=True) + def companion_stack_outputs(self, get_companion_stack_outputs): + self.companion_stack_outputs = get_companion_stack_outputs + @parameterized.expand( [ - "combination/function_with_mq", - "combination/function_with_mq_using_autogen_role", + ("combination/function_with_mq", "MQBrokerName", "MQBrokerUserSecretName", "PreCreatedSubnetOne"), + ( + "combination/function_with_mq_using_autogen_role", + "MQBrokerName2", + "MQBrokerUserSecretName2", + "PreCreatedSubnetTwo", + ), ] ) - def test_function_with_mq(self, file_name): - self.create_and_verify_stack(file_name) + def test_function_with_mq(self, file_name, mq_broker, mq_secret, subnet_key): + companion_stack_outputs = self.companion_stack_outputs + parameters = self.get_parameters(companion_stack_outputs, subnet_key) + secret_name = mq_secret + "-" + generate_suffix() + parameters.append(self.generate_parameter(mq_secret, secret_name)) + secret_name = mq_broker + "-" + generate_suffix() + parameters.append(self.generate_parameter(mq_broker, secret_name)) + + self.create_and_verify_stack(file_name, parameters) mq_client = self.client_provider.mq_client mq_broker_id = self.get_physical_id_by_type("AWS::AmazonMQ::Broker") @@ -32,6 +54,13 @@ def test_function_with_mq(self, file_name): self.assertEqual(event_source_mapping_function_arn, lambda_function_arn) self.assertEqual(event_source_mapping_mq_broker_arn, mq_broker_arn) + def get_parameters(self, dictionary, subnet_key): + parameters = [] + parameters.append(self.generate_parameter("PreCreatedVpc", dictionary["PreCreatedVpc"])) + parameters.append(self.generate_parameter(subnet_key, dictionary[subnet_key])) + parameters.append(self.generate_parameter("PreCreatedInternetGateway", dictionary["PreCreatedInternetGateway"])) + return parameters + def get_broker_summary(mq_broker_id, mq_client): broker_summaries = mq_client.list_brokers()["BrokerSummaries"] diff --git a/integration/combination/test_function_with_msk.py b/integration/combination/test_function_with_msk.py index cb855f1db..8935a4a33 100644 --- a/integration/combination/test_function_with_msk.py +++ b/integration/combination/test_function_with_msk.py @@ -1,15 +1,34 @@ +from unittest.case import skipIf + +import pytest + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support, generate_suffix +from integration.config.service_names import MSK +@skipIf(current_region_does_not_support([MSK]), "MSK is not supported in this testing region") class TestFunctionWithMsk(BaseTest): + @pytest.fixture(autouse=True) + def companion_stack_outputs(self, get_companion_stack_outputs): + self.companion_stack_outputs = get_companion_stack_outputs + def test_function_with_msk_trigger(self): - self._common_validations_for_MSK("combination/function_with_msk") + companion_stack_outputs = self.companion_stack_outputs + parameters = self.get_parameters(companion_stack_outputs) + cluster_name = "MskCluster-" + generate_suffix() + parameters.append(self.generate_parameter("MskClusterName", cluster_name)) + self._common_validations_for_MSK("combination/function_with_msk", parameters) def test_function_with_msk_trigger_using_manage_policy(self): - self._common_validations_for_MSK("combination/function_with_msk_using_managed_policy") + companion_stack_outputs = self.companion_stack_outputs + parameters = self.get_parameters(companion_stack_outputs) + cluster_name = "MskCluster2-" + generate_suffix() + parameters.append(self.generate_parameter("MskClusterName2", cluster_name)) + self._common_validations_for_MSK("combination/function_with_msk_using_managed_policy", parameters) - def _common_validations_for_MSK(self, file_name): - self.create_and_verify_stack(file_name) + def _common_validations_for_MSK(self, file_name, parameters): + self.create_and_verify_stack(file_name, parameters) kafka_client = self.client_provider.kafka_client @@ -32,3 +51,9 @@ def _common_validations_for_MSK(self, file_name): self.assertEqual(event_source_mapping_function_arn, lambda_function_arn) self.assertEqual(event_source_mapping_kafka_cluster_arn, msk_cluster_arn) + + def get_parameters(self, dictionary): + parameters = [] + parameters.append(self.generate_parameter("PreCreatedSubnetOne", dictionary["PreCreatedSubnetOne"])) + parameters.append(self.generate_parameter("PreCreatedSubnetTwo", dictionary["PreCreatedSubnetTwo"])) + return parameters diff --git a/integration/combination/test_function_with_schedule.py b/integration/combination/test_function_with_schedule.py index 746355a38..304f923a1 100644 --- a/integration/combination/test_function_with_schedule.py +++ b/integration/combination/test_function_with_schedule.py @@ -1,16 +1,21 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support, generate_suffix +from integration.config.service_names import SCHEDULE_EVENT +@skipIf(current_region_does_not_support([SCHEDULE_EVENT]), "ScheduleEvent is not supported in this testing region") class TestFunctionWithSchedule(BaseTest): def test_function_with_schedule(self): - self.create_and_verify_stack("combination/function_with_schedule") + schedule_name = "TestSchedule" + generate_suffix() + parameters = [self.generate_parameter("ScheduleName", schedule_name)] - stack_outputs = self.get_stack_outputs() + self.create_and_verify_stack("combination/function_with_schedule", parameters) cloud_watch_events_client = self.client_provider.cloudwatch_event_client # get the cloudwatch schedule rule - schedule_name = stack_outputs["ScheduleName"] cw_rule_result = cloud_watch_events_client.describe_rule(Name=schedule_name) # checking if the name, description and state properties are correct diff --git a/integration/combination/test_function_with_schedule_dlq_and_retry_policy.py b/integration/combination/test_function_with_schedule_dlq_and_retry_policy.py index 663dfc0cb..7079ad02b 100644 --- a/integration/combination/test_function_with_schedule_dlq_and_retry_policy.py +++ b/integration/combination/test_function_with_schedule_dlq_and_retry_policy.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CWE_CWS_DLQ +@skipIf(current_region_does_not_support([CWE_CWS_DLQ]), "CweCwsDlq is not supported in this testing region") class TestFunctionWithScheduleDlqAndRetryPolicy(BaseTest): def test_function_with_schedule(self): self.create_and_verify_stack("combination/function_with_schedule_dlq_and_retry_policy") diff --git a/integration/combination/test_function_with_schedule_dlq_generated.py b/integration/combination/test_function_with_schedule_dlq_generated.py index 7d79937bc..0d815e860 100644 --- a/integration/combination/test_function_with_schedule_dlq_generated.py +++ b/integration/combination/test_function_with_schedule_dlq_generated.py @@ -1,7 +1,12 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest from integration.helpers.common_api import get_queue_policy +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CWE_CWS_DLQ +@skipIf(current_region_does_not_support([CWE_CWS_DLQ]), "CweCwsDlq is not supported in this testing region") class TestFunctionWithScheduleDlqGenerated(BaseTest): def test_function_with_schedule(self): self.create_and_verify_stack("combination/function_with_schedule_dlq_generated") diff --git a/integration/combination/test_function_with_signing_profile.py b/integration/combination/test_function_with_signing_profile.py index f83f90836..b20823ad0 100644 --- a/integration/combination/test_function_with_signing_profile.py +++ b/integration/combination/test_function_with_signing_profile.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CODE_SIGN class TestDependsOn(BaseTest): - def test_depends_on(self): + @skipIf(current_region_does_not_support([CODE_SIGN]), "CodeSign is not supported in this testing region") + def test_function_with_signing_profile(self): self.create_and_verify_stack("combination/function_with_signing_profile") diff --git a/integration/combination/test_function_with_sns.py b/integration/combination/test_function_with_sns.py index 9f7dd597e..2562099af 100644 --- a/integration/combination/test_function_with_sns.py +++ b/integration/combination/test_function_with_sns.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import SNS +@skipIf(current_region_does_not_support([SNS]), "SNS is not supported in this testing region") class TestFunctionWithSns(BaseTest): def test_function_with_sns_bucket_trigger(self): self.create_and_verify_stack("combination/function_with_sns") diff --git a/integration/combination/test_function_with_sqs.py b/integration/combination/test_function_with_sqs.py index e5f54cc2d..022b65a3d 100644 --- a/integration/combination/test_function_with_sqs.py +++ b/integration/combination/test_function_with_sqs.py @@ -1,8 +1,13 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import SQS -class TestFunctionWithSns(BaseTest): - def test_function_with_sns_bucket_trigger(self): +@skipIf(current_region_does_not_support([SQS]), "SQS is not supported in this testing region") +class TestFunctionWithSQS(BaseTest): + def test_function_with_sqs_bucket_trigger(self): self.create_and_verify_stack("combination/function_with_sqs") sqs_client = self.client_provider.sqs_client diff --git a/integration/combination/test_function_with_user_pool_event.py b/integration/combination/test_function_with_user_pool_event.py index ba9ca9f26..df73bb0e7 100644 --- a/integration/combination/test_function_with_user_pool_event.py +++ b/integration/combination/test_function_with_user_pool_event.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import COGNITO +@skipIf(current_region_does_not_support([COGNITO]), "Cognito is not supported in this testing region") class TestFunctionWithUserPoolEvent(BaseTest): def test_function_with_user_pool_event(self): self.create_and_verify_stack("combination/function_with_userpool_event") diff --git a/integration/combination/test_http_api_with_auth.py b/integration/combination/test_http_api_with_auth.py index 963e44738..315e9c371 100644 --- a/integration/combination/test_http_api_with_auth.py +++ b/integration/combination/test_http_api_with_auth.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import HTTP_API +@skipIf(current_region_does_not_support([HTTP_API]), "HttpApi is not supported in this testing region") class TestFunctionWithUserPoolEvent(BaseTest): def test_function_with_user_pool_event(self): self.create_and_verify_stack("combination/http_api_with_auth") diff --git a/integration/combination/test_http_api_with_cors.py b/integration/combination/test_http_api_with_cors.py index a51db8303..1d0f734ad 100644 --- a/integration/combination/test_http_api_with_cors.py +++ b/integration/combination/test_http_api_with_cors.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import HTTP_API +@skipIf(current_region_does_not_support([HTTP_API]), "HttpApi is not supported in this testing region") class TestHttpApiWithCors(BaseTest): def test_cors(self): self.create_and_verify_stack("combination/http_api_with_cors") diff --git a/integration/combination/test_http_api_with_disable_execute_api_endpoint.py b/integration/combination/test_http_api_with_disable_execute_api_endpoint.py index 3012e1b85..35931ba65 100644 --- a/integration/combination/test_http_api_with_disable_execute_api_endpoint.py +++ b/integration/combination/test_http_api_with_disable_execute_api_endpoint.py @@ -1,8 +1,13 @@ +from unittest.case import skipIf + from parameterized import parameterized from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CUSTOM_DOMAIN +@skipIf(current_region_does_not_support([CUSTOM_DOMAIN]), "CustomDomain is not supported in this testing region") class TestHttpApiWithDisableExecuteApiEndpoint(BaseTest): @parameterized.expand( [ diff --git a/integration/combination/test_state_machine_with_cwe_dlq_and_retry_policy.py b/integration/combination/test_state_machine_with_cwe_dlq_and_retry_policy.py index 7b957416a..79484c787 100644 --- a/integration/combination/test_state_machine_with_cwe_dlq_and_retry_policy.py +++ b/integration/combination/test_state_machine_with_cwe_dlq_and_retry_policy.py @@ -1,6 +1,11 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CWE_CWS_DLQ +@skipIf(current_region_does_not_support([CWE_CWS_DLQ]), "CweCwsDlq is not supported in this testing region") class TestStateMachineWithCweDlqAndRetryPolicy(BaseTest): def test_state_machine_with_api(self): self.create_and_verify_stack("combination/state_machine_with_cwe_with_dlq_and_retry_policy") diff --git a/integration/combination/test_state_machine_with_cwe_dlq_generated.py b/integration/combination/test_state_machine_with_cwe_dlq_generated.py index 4bea0d299..c5071c187 100644 --- a/integration/combination/test_state_machine_with_cwe_dlq_generated.py +++ b/integration/combination/test_state_machine_with_cwe_dlq_generated.py @@ -1,7 +1,12 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest from integration.helpers.common_api import get_policy_statements, get_queue_policy +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CWE_CWS_DLQ +@skipIf(current_region_does_not_support([CWE_CWS_DLQ]), "CweCwsDlq is not supported in this testing region") class TestStateMachineWithCweDlqGenerated(BaseTest): def test_state_machine_with_cwe(self): self.create_and_verify_stack("combination/state_machine_with_cwe_dlq_generated") diff --git a/integration/combination/test_state_machine_with_policy_templates.py b/integration/combination/test_state_machine_with_policy_templates.py index f4112601d..f887af6bb 100644 --- a/integration/combination/test_state_machine_with_policy_templates.py +++ b/integration/combination/test_state_machine_with_policy_templates.py @@ -1,7 +1,12 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest from integration.helpers.common_api import get_policy_statements +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import SQS +@skipIf(current_region_does_not_support([SQS]), "SQS is not supported in this testing region") class TestStateMachineWithPolicyTemplates(BaseTest): def test_with_policy_templates(self): self.create_and_verify_stack("combination/state_machine_with_policy_templates") diff --git a/integration/combination/test_state_machine_with_schedule_dlq_and_retry_policy.py b/integration/combination/test_state_machine_with_schedule_dlq_and_retry_policy.py index 3166ae7af..522377112 100644 --- a/integration/combination/test_state_machine_with_schedule_dlq_and_retry_policy.py +++ b/integration/combination/test_state_machine_with_schedule_dlq_and_retry_policy.py @@ -1,7 +1,12 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest from integration.helpers.common_api import get_policy_statements +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CWE_CWS_DLQ +@skipIf(current_region_does_not_support([CWE_CWS_DLQ]), "CweCwsDlq is not supported in this testing region") class TestStateMachineWithScheduleDlqAndRetryPolicy(BaseTest): def test_state_machine_with_schedule(self): self.create_and_verify_stack("combination/state_machine_with_schedule_dlq_and_retry_policy") diff --git a/integration/combination/test_state_machine_with_schedule_dlq_generated.py b/integration/combination/test_state_machine_with_schedule_dlq_generated.py index 64918f9ec..557faf162 100644 --- a/integration/combination/test_state_machine_with_schedule_dlq_generated.py +++ b/integration/combination/test_state_machine_with_schedule_dlq_generated.py @@ -1,7 +1,12 @@ +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest from integration.helpers.common_api import get_queue_policy +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import CWE_CWS_DLQ +@skipIf(current_region_does_not_support([CWE_CWS_DLQ]), "CweCwsDlq is not supported in this testing region") class TestStateMachineWithScheduleDlqGenerated(BaseTest): def test_state_machine_with_schedule(self): self.create_and_verify_stack("combination/state_machine_with_schedule_dlq_generated") diff --git a/integration/config/region_service_exclusion.yaml b/integration/config/region_service_exclusion.yaml index d2241a114..1258a5ef9 100644 --- a/integration/config/region_service_exclusion.yaml +++ b/integration/config/region_service_exclusion.yaml @@ -44,6 +44,11 @@ regions: - IoT - GatewayResponses - HttpApi + - MSK + - MQ + - CodeSign + - CweCwsDlq + - Mode - ARM cn-northwest-1: - ServerlessRepo diff --git a/integration/config/service_names.py b/integration/config/service_names.py new file mode 100644 index 000000000..d29d26d47 --- /dev/null +++ b/integration/config/service_names.py @@ -0,0 +1,22 @@ +COGNITO = "Cognito" +SERVERLESS_REPO = "ServerlessRepo" +MODE = "Mode" +XRAY = "XRay" +LAYERS = "Layers" +HTTP_API = "HttpApi" +IOT = "IoT" +CODE_DEPLOY = "CodeDeploy" +ARM = "ARM" +GATEWAY_RESPONSES = "GatewayResponses" +MSK = "MSK" +KMS = "KMS" +CWE_CWS_DLQ = "CweCwsDlq" +CODE_SIGN = "CodeSign" +MQ = "MQ" +USAGE_PLANS = "UsagePlans" +SCHEDULE_EVENT = "ScheduleEvent" +DYNAMO_DB = "DynamoDB" +KINESIS = "Kinesis" +SNS = "SNS" +SQS = "SQS" +CUSTOM_DOMAIN = "CustomDomain" diff --git a/integration/conftest.py b/integration/conftest.py new file mode 100644 index 000000000..4d6472217 --- /dev/null +++ b/integration/conftest.py @@ -0,0 +1,130 @@ +import boto3 +import botocore +import pytest +from botocore.exceptions import ClientError +import logging + +from integration.helpers.base_test import S3_BUCKET_PREFIX +from integration.helpers.client_provider import ClientProvider +from integration.helpers.deployer.exceptions.exceptions import ThrottlingError +from integration.helpers.deployer.utils.retry import retry_with_exponential_backoff_and_jitter +from integration.helpers.stack import Stack + +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + +LOG = logging.getLogger(__name__) + +COMPANION_STACK_NAME = "sam-integ-stack-companion" +COMPANION_STACK_TEMPLATE = "companion-stack.yaml" + + +def _get_all_buckets(): + s3 = boto3.resource("s3") + return s3.buckets.all() + + +def _clean_bucket(s3_bucket_name, s3_client): + """ + Empties and deletes the bucket used for the tests + """ + s3 = boto3.resource("s3") + bucket = s3.Bucket(s3_bucket_name) + object_summary_iterator = bucket.objects.all() + + for object_summary in object_summary_iterator: + try: + s3_client.delete_object(Key=object_summary.key, Bucket=s3_bucket_name) + except ClientError as e: + LOG.error("Unable to delete object %s from bucket %s", object_summary.key, s3_bucket_name, exc_info=e) + try: + s3_client.delete_bucket(Bucket=s3_bucket_name) + except ClientError as e: + LOG.error("Unable to delete bucket %s", s3_bucket_name, exc_info=e) + + +@pytest.fixture(scope="session") +def clean_all_integ_buckets(): + buckets = _get_all_buckets() + s3_client = ClientProvider().s3_client + for bucket in buckets: + if bucket.name.startswith(S3_BUCKET_PREFIX): + _clean_bucket(bucket.name, s3_client) + + +@pytest.fixture() +def setup_companion_stack_once(tmpdir_factory, get_prefix): + tests_integ_dir = Path(__file__).resolve().parents[1] + template_foler = Path(tests_integ_dir, "integration", "setup") + companion_stack_tempalte_path = Path(template_foler, COMPANION_STACK_TEMPLATE) + cfn_client = ClientProvider().cfn_client + output_dir = tmpdir_factory.mktemp("data") + stack_name = get_prefix + COMPANION_STACK_NAME + if _stack_exists(stack_name): + return + companion_stack = Stack(stack_name, companion_stack_tempalte_path, cfn_client, output_dir) + companion_stack.create() + + +@pytest.fixture() +def delete_companion_stack_once(get_prefix): + if not get_prefix: + ClientProvider().cfn_client.delete_stack(StackName=COMPANION_STACK_NAME) + + +@retry_with_exponential_backoff_and_jitter(ThrottlingError, 5, 360) +def get_stack_description(stack_name): + try: + stack_description = ClientProvider().cfn_client.describe_stacks(StackName=stack_name) + return stack_description + except botocore.exceptions.ClientError as ex: + if "Throttling" in str(ex): + raise ThrottlingError(stack_name=stack_name, msg=str(ex)) + raise ex + + +def get_stack_outputs(stack_description): + if not stack_description: + return {} + output_list = stack_description["Stacks"][0]["Outputs"] + return {output["OutputKey"]: output["OutputValue"] for output in output_list} + + +@pytest.fixture() +def get_companion_stack_outputs(get_prefix): + companion_stack_description = get_stack_description(get_prefix + COMPANION_STACK_NAME) + return get_stack_outputs(companion_stack_description) + + +@pytest.fixture() +def get_prefix(request): + prefix = "" + if request.config.getoption("--prefix"): + prefix = request.config.getoption("--prefix") + "-" + return prefix + + +def pytest_addoption(parser): + parser.addoption( + "--prefix", + default=None, + help="the prefix of the stack", + ) + + +@retry_with_exponential_backoff_and_jitter(ThrottlingError, 5, 360) +def _stack_exists(stack_name): + cloudformation = boto3.resource("cloudformation") + stack = cloudformation.Stack(stack_name) + try: + stack.stack_status + except ClientError as ex: + if "does not exist" in str(ex): + return False + if "Throttling" in str(ex): + raise ThrottlingError(stack_name=stack_name, msg=str(ex)) + raise ex + + return True diff --git a/integration/helpers/base_test.py b/integration/helpers/base_test.py index 06a10a56b..78f756c8f 100644 --- a/integration/helpers/base_test.py +++ b/integration/helpers/base_test.py @@ -2,9 +2,13 @@ import logging import os +import botocore +import pytest import requests from integration.helpers.client_provider import ClientProvider +from integration.helpers.deployer.exceptions.exceptions import ThrottlingError +from integration.helpers.deployer.utils.retry import retry_with_exponential_backoff_and_jitter from integration.helpers.resource import generate_suffix, create_bucket, verify_stack_resources from integration.helpers.yaml_utils import dump_yaml, load_yaml from samtranslator.yaml_helper import yaml_parse @@ -16,10 +20,7 @@ from unittest.case import TestCase import boto3 -import pytest -import yaml from botocore.exceptions import ClientError -from botocore.config import Config from integration.helpers.deployer.deployer import Deployer from integration.helpers.template import transform_template @@ -31,7 +32,12 @@ class BaseTest(TestCase): + @pytest.fixture(autouse=True) + def prefix(self, get_prefix): + self.pipeline_prefix = get_prefix + @classmethod + @pytest.mark.usefixtures("get_prefix") def setUpClass(cls): cls.FUNCTION_OUTPUT = "hello" cls.tests_integ_dir = Path(__file__).resolve().parents[1] @@ -128,7 +134,7 @@ def tearDown(self): if os.path.exists(self.sub_input_file_path): os.remove(self.sub_input_file_path) - def create_and_verify_stack(self, file_path, parameters=None): + def create_stack(self, file_path, parameters=None): """ Creates the Cloud Formation stack and verifies it against the expected result @@ -141,15 +147,30 @@ def create_and_verify_stack(self, file_path, parameters=None): List of parameters """ folder, file_name = file_path.split("/") - # add a folder name before file name to avoid possible collisions between - # files in the single and combination folder - self.output_file_path = str(Path(self.output_dir, "cfn_" + folder + "_" + file_name + ".yaml")) - self.expected_resource_path = str(Path(self.expected_dir, folder, file_name + ".json")) - self.stack_name = STACK_NAME_PREFIX + file_name.replace("_", "-") + "-" + generate_suffix() + self.generate_out_put_file_path(folder, file_name) + self.stack_name = ( + self.pipeline_prefix + STACK_NAME_PREFIX + file_name.replace("_", "-") + "-" + generate_suffix() + ) self._fill_template(folder, file_name) self.transform_template() self.deploy_stack(parameters) + + def create_and_verify_stack(self, file_path, parameters=None): + """ + Creates the Cloud Formation stack and verifies it against the expected + result + + Parameters + ---------- + file_path : string + Template file name, format "folder_name/file_name" + parameters : list + List of parameters + """ + folder, file_name = file_path.split("/") + self.create_stack(file_path, parameters) + self.expected_resource_path = str(Path(self.expected_dir, folder, file_name + ".json")) self.verify_stack() def update_stack(self, file_path, parameters=None): @@ -169,9 +190,7 @@ def update_stack(self, file_path, parameters=None): os.remove(self.sub_input_file_path) folder, file_name = file_path.split("/") - # add a folder name before file name to avoid possible collisions between - # files in the single and combination folder - self.output_file_path = str(Path(self.output_dir, "cfn_" + folder + "_" + file_name + ".yaml")) + self.generate_out_put_file_path(folder, file_name) self._fill_template(folder, file_name) self.transform_template() @@ -193,9 +212,7 @@ def update_and_verify_stack(self, file_path, parameters=None): raise Exception("Stack not created.") folder, file_name = file_path.split("/") - # add a folder name before file name to avoid possible collisions between - # files in the single and combination folder - self.output_file_path = str(Path(self.output_dir, "cfn_" + folder + "_" + file_name + ".yaml")) + self.generate_out_put_file_path(folder, file_name) self.expected_resource_path = str(Path(self.expected_dir, folder, file_name + ".json")) self._fill_template(folder, file_name) @@ -203,6 +220,13 @@ def update_and_verify_stack(self, file_path, parameters=None): self.deploy_stack(parameters) self.verify_stack(end_state="UPDATE_COMPLETE") + def generate_out_put_file_path(self, folder_name, file_name): + # add a folder name before file name to avoid possible collisions between + # files in the single and combination folder + self.output_file_path = str( + Path(self.output_dir, "cfn_" + folder_name + "_" + file_name + generate_suffix() + ".yaml") + ) + def transform_template(self): transform_template(self.sub_input_file_path, self.output_file_path) @@ -349,7 +373,7 @@ def _fill_template(self, folder, file_name): input_file_path = str(Path(self.template_dir, folder, file_name + ".yaml")) # add a folder name before file name to avoid possible collisions between # files in the single and combination folder - updated_template_path = str(Path(self.output_dir, "sub_" + folder + "_" + file_name + ".yaml")) + updated_template_path = self.output_file_path.split(".yaml")[0] + "_sub" + ".yaml" with open(input_file_path) as f: data = f.read() for key, _ in self.code_key_to_file.items(): @@ -415,15 +439,25 @@ def deploy_stack(self, parameters=None): self.deployer.execute_changeset(result["Id"], self.stack_name) self.deployer.wait_for_execute(self.stack_name, changeset_type) - self.stack_description = self.client_provider.cfn_client.describe_stacks(StackName=self.stack_name) + self._get_stack_description() self.stack_resources = self.client_provider.cfn_client.list_stack_resources(StackName=self.stack_name) + @retry_with_exponential_backoff_and_jitter(ThrottlingError, 5, 360) + def _get_stack_description(self): + try: + self.stack_description = self.client_provider.cfn_client.describe_stacks(StackName=self.stack_name) + except botocore.exceptions.ClientError as ex: + if "Throttling" in str(ex): + raise ThrottlingError(stack_name=self.stack_name, msg=str(ex)) + raise + def verify_stack(self, end_state="CREATE_COMPLETE"): """ Gets and compares the Cloud Formation stack against the expect result file """ # verify if the stack was successfully created self.assertEqual(self.stack_description["Stacks"][0]["StackStatus"], end_state) + assert self.stack_description["Stacks"][0]["StackStatus"] == end_state # verify if the stack contains the expected resources error = verify_stack_resources(self.expected_resource_path, self.stack_resources) if error: @@ -470,3 +504,13 @@ def get_default_test_template_parameters(self): }, ] return parameters + + @staticmethod + def generate_parameter(key, value, previous_value=False, resolved_value="string"): + parameter = { + "ParameterKey": key, + "ParameterValue": value, + "UsePreviousValue": previous_value, + "ResolvedValue": resolved_value, + } + return parameter diff --git a/integration/helpers/deployer/deployer.py b/integration/helpers/deployer/deployer.py index 8a4422a29..3a0c9ff99 100644 --- a/integration/helpers/deployer/deployer.py +++ b/integration/helpers/deployer/deployer.py @@ -36,6 +36,7 @@ from integration.helpers.deployer.utils.colors import DeployColor from integration.helpers.deployer.exceptions import exceptions as deploy_exceptions +from integration.helpers.deployer.utils.retry import retry, retry_with_exponential_backoff_and_jitter from integration.helpers.deployer.utils.table_print import ( pprint_column_names, pprint_columns, @@ -428,17 +429,22 @@ def wait_for_execute(self, stack_name, changeset_type): # Poll every 30 seconds. Polling too frequently risks hitting rate limits # on CloudFormation's DescribeStacks API waiter_config = {"Delay": 30, "MaxAttempts": 120} + self._wait(stack_name, waiter, waiter_config) + outputs = self.get_stack_outputs(stack_name=stack_name, echo=False) + if outputs: + self._display_stack_outputs(outputs) + + @retry_with_exponential_backoff_and_jitter(deploy_exceptions.ThrottlingError, 5, 360) + def _wait(self, stack_name, waiter, waiter_config): try: waiter.wait(StackName=stack_name, WaiterConfig=waiter_config) except botocore.exceptions.WaiterError as ex: LOG.debug("Execute changeset waiter exception", exc_info=ex) - - raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) - - outputs = self.get_stack_outputs(stack_name=stack_name, echo=False) - if outputs: - self._display_stack_outputs(outputs) + if "Throttling" in str(ex): + raise deploy_exceptions.ThrottlingError(stack_name=stack_name, msg=str(ex)) + else: + raise deploy_exceptions.DeployFailedError(stack_name=stack_name, msg=str(ex)) def create_and_wait_for_changeset( self, stack_name, cfn_template, parameter_values, capabilities, role_arn, notification_arns, s3_uploader, tags @@ -477,6 +483,7 @@ def _display_stack_outputs(self, stack_outputs, **kwargs): ) newline_per_item(stack_outputs, counter) + @retry_with_exponential_backoff_and_jitter(deploy_exceptions.ThrottlingError, 5, 360) def get_stack_outputs(self, stack_name, echo=True): try: stacks_description = self._client.describe_stacks(StackName=stack_name) @@ -491,4 +498,7 @@ def get_stack_outputs(self, stack_name, echo=True): return None except botocore.exceptions.ClientError as ex: - raise deploy_exceptions.DeployStackOutPutFailedError(stack_name=stack_name, msg=str(ex)) + if "Throttling" in str(ex): + raise deploy_exceptions.ThrottlingError(stack_name=stack_name, msg=str(ex)) + else: + raise deploy_exceptions.DeployStackOutPutFailedError(stack_name=stack_name, msg=str(ex)) diff --git a/integration/helpers/deployer/exceptions/exceptions.py b/integration/helpers/deployer/exceptions/exceptions.py index 3dee92caa..582ef1f74 100644 --- a/integration/helpers/deployer/exceptions/exceptions.py +++ b/integration/helpers/deployer/exceptions/exceptions.py @@ -63,3 +63,13 @@ def __init__(self, msg): message_fmt = "{msg} : deployment s3 bucket is in a different region, try sam deploy --guided" super(DeployBucketInDifferentRegionError, self).__init__(message=message_fmt.format(msg=self.msg)) + + +class ThrottlingError(UserException): + def __init__(self, stack_name, msg): + self.stack_name = stack_name + self.msg = msg + + message_fmt = "Throttling Issue occurred: {stack_name}, {msg}" + + super(ThrottlingError, self).__init__(message=message_fmt.format(stack_name=self.stack_name, msg=msg)) diff --git a/integration/helpers/deployer/utils/retry.py b/integration/helpers/deployer/utils/retry.py index ab6b07258..1875ebb40 100644 --- a/integration/helpers/deployer/utils/retry.py +++ b/integration/helpers/deployer/utils/retry.py @@ -2,6 +2,7 @@ Retry decorator to retry decorated function based on Exception with exponential backoff and number of attempts built-in. """ import math +import random import time from functools import wraps @@ -38,3 +39,38 @@ def wrapper(*args, **kwargs): return wrapper return retry_wrapper + + +def retry_with_exponential_backoff_and_jitter(exc, attempts=3, delay=0.05, exc_raise=Exception, exc_raise_msg=""): + """ + Retry decorator which defaults to 3 attempts based on exponential backoff + and a delay of 50ms. + After retries are exhausted, a custom Exception and Error message are raised. + + :param exc: Exception to be caught for retry + :param attempts: number of attempts before exception is allowed to be raised. + :param delay: an initial delay which will exponentially increase based on the retry attempt. + :param exc_raise: Final Exception to raise. + :param exc_raise_msg: Final message for the Exception to be raised. + :return: + """ + + def retry_wrapper(func): + @wraps(func) + def wrapper(*args, **kwargs): + remaining_attempts = attempts + retry_attempt = 1 + + while remaining_attempts >= 1: + try: + return func(*args, **kwargs) + except exc: + sleep_time = random.uniform(0, math.pow(2, retry_attempt) * delay) + time.sleep(sleep_time) + retry_attempt = retry_attempt + 1 + remaining_attempts = remaining_attempts - 1 + raise exc_raise(exc_raise_msg) + + return wrapper + + return retry_wrapper diff --git a/integration/helpers/stack.py b/integration/helpers/stack.py new file mode 100644 index 000000000..d55faa6ce --- /dev/null +++ b/integration/helpers/stack.py @@ -0,0 +1,75 @@ +import botocore + +from integration.helpers.deployer.deployer import Deployer +from integration.helpers.deployer.exceptions.exceptions import ThrottlingError +from integration.helpers.deployer.utils.retry import retry_with_exponential_backoff_and_jitter +from integration.helpers.resource import generate_suffix +from integration.helpers.template import transform_template + +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + + +class Stack: + def __init__(self, stack_name, template_path, cfn_client, output_dir): + self.stack_name = stack_name + self.template_path = str(template_path) + self.cfn_client = cfn_client + self.deployer = Deployer(cfn_client) + self.output_dir = str(output_dir) + self.stack_description = None + self.stack_resources = None + + def create(self): + output_template_path = self._generate_output_file_path(self.template_path, self.output_dir) + transform_template(self.template_path, output_template_path) + self._deploy_stack(output_template_path) + + def delete(self): + self.cfn_client.delete_stack(StackName=self.stack_name) + + def get_stack_outputs(self): + if not self.stack_description: + return {} + output_list = self.stack_description["Stacks"][0]["Outputs"] + return {output["OutputKey"]: output["OutputValue"] for output in output_list} + + def _deploy_stack(self, output_file_path, parameters=None): + """ + Deploys the current cloud formation stack + """ + with open(output_file_path) as cfn_file: + result, changeset_type = self.deployer.create_and_wait_for_changeset( + stack_name=self.stack_name, + cfn_template=cfn_file.read(), + parameter_values=[] if parameters is None else parameters, + capabilities=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], + role_arn=None, + notification_arns=[], + s3_uploader=None, + tags=[], + ) + self.deployer.execute_changeset(result["Id"], self.stack_name) + self.deployer.wait_for_execute(self.stack_name, changeset_type) + + self._get_stack_description() + self.stack_resources = self.cfn_client.list_stack_resources(StackName=self.stack_name) + + @retry_with_exponential_backoff_and_jitter(ThrottlingError, 5, 360) + def _get_stack_description(self): + try: + self.stack_description = self.cfn_client.describe_stacks(StackName=self.stack_name) + except botocore.exceptions.ClientError as ex: + if "Throttling" in str(ex): + raise ThrottlingError(stack_name=self.stack_name, msg=str(ex)) + raise + + @staticmethod + def _generate_output_file_path(file_path, output_dir): + # add a folder name before file name to avoid possible collisions between + # files in the single and combination folder + folder_name = file_path.split("/")[-2] + file_name = file_path.split("/")[-1].split(".")[0] + return str(Path(output_dir, "cfn_" + folder_name + "_" + file_name + generate_suffix() + ".yaml")) diff --git a/integration/metrics/test_metrics_integration.py b/integration/metrics/test_metrics_integration.py index e2a69b757..1fc89d718 100644 --- a/integration/metrics/test_metrics_integration.py +++ b/integration/metrics/test_metrics_integration.py @@ -59,7 +59,7 @@ def get_unique_namespace(self): namespace = "SinglePublishTest-{}".format(uuid.uuid1()) def get_metric_data(self, namespace, metric_name, dimensions, start_time, end_time, stat="Sum"): - retries = 3 + retries = 20 while retries > 0: retries -= 1 response = self.cw_client.get_metric_data( diff --git a/integration/resources/expected/combination/function_with_mq.json b/integration/resources/expected/combination/function_with_mq.json index 32f9f0422..09f23b169 100644 --- a/integration/resources/expected/combination/function_with_mq.json +++ b/integration/resources/expected/combination/function_with_mq.json @@ -2,14 +2,10 @@ { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" }, { "LogicalResourceId":"MyLambdaExecutionRole", "ResourceType":"AWS::IAM::Role" }, { "LogicalResourceId":"PublicSubnetRouteTableAssociation", "ResourceType":"AWS::EC2::SubnetRouteTableAssociation" }, - { "LogicalResourceId":"AttachGateway", "ResourceType":"AWS::EC2::VPCGatewayAttachment" }, { "LogicalResourceId":"MQSecurityGroup", "ResourceType":"AWS::EC2::SecurityGroup" }, - { "LogicalResourceId":"MyVpc", "ResourceType":"AWS::EC2::VPC" }, { "LogicalResourceId":"MyMqBroker", "ResourceType":"AWS::AmazonMQ::Broker" }, - { "LogicalResourceId":"PublicSubnet", "ResourceType":"AWS::EC2::Subnet" }, { "LogicalResourceId":"RouteTable", "ResourceType":"AWS::EC2::RouteTable" }, { "LogicalResourceId":"MQBrokerUserSecret", "ResourceType":"AWS::SecretsManager::Secret" }, { "LogicalResourceId":"MyLambdaFunctionMyMqEvent", "ResourceType":"AWS::Lambda::EventSourceMapping" }, - { "LogicalResourceId":"InternetGateway", "ResourceType":"AWS::EC2::InternetGateway" }, { "LogicalResourceId":"Route", "ResourceType":"AWS::EC2::Route" } ] \ No newline at end of file diff --git a/integration/resources/expected/combination/function_with_mq_using_autogen_role.json b/integration/resources/expected/combination/function_with_mq_using_autogen_role.json index 128c0787c..d486b2323 100644 --- a/integration/resources/expected/combination/function_with_mq_using_autogen_role.json +++ b/integration/resources/expected/combination/function_with_mq_using_autogen_role.json @@ -2,14 +2,10 @@ { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" }, { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" }, { "LogicalResourceId":"PublicSubnetRouteTableAssociation", "ResourceType":"AWS::EC2::SubnetRouteTableAssociation" }, - { "LogicalResourceId":"AttachGateway", "ResourceType":"AWS::EC2::VPCGatewayAttachment" }, { "LogicalResourceId":"MQSecurityGroup", "ResourceType":"AWS::EC2::SecurityGroup" }, - { "LogicalResourceId":"MyVpc", "ResourceType":"AWS::EC2::VPC" }, { "LogicalResourceId":"MyMqBroker", "ResourceType":"AWS::AmazonMQ::Broker" }, - { "LogicalResourceId":"PublicSubnet", "ResourceType":"AWS::EC2::Subnet" }, { "LogicalResourceId":"RouteTable", "ResourceType":"AWS::EC2::RouteTable" }, { "LogicalResourceId":"MQBrokerUserSecret", "ResourceType":"AWS::SecretsManager::Secret" }, { "LogicalResourceId":"MyLambdaFunctionMyMqEvent", "ResourceType":"AWS::Lambda::EventSourceMapping" }, - { "LogicalResourceId":"InternetGateway", "ResourceType":"AWS::EC2::InternetGateway" }, { "LogicalResourceId":"Route", "ResourceType":"AWS::EC2::Route" } ] \ No newline at end of file diff --git a/integration/resources/expected/combination/function_with_msk.json b/integration/resources/expected/combination/function_with_msk.json index 0b96aed90..f39a0f703 100644 --- a/integration/resources/expected/combination/function_with_msk.json +++ b/integration/resources/expected/combination/function_with_msk.json @@ -2,8 +2,5 @@ { "LogicalResourceId":"MyMskStreamProcessor", "ResourceType":"AWS::Lambda::Function" }, { "LogicalResourceId":"MyLambdaExecutionRole", "ResourceType":"AWS::IAM::Role" }, { "LogicalResourceId":"MyMskCluster", "ResourceType":"AWS::MSK::Cluster" }, - { "LogicalResourceId":"MyVpc", "ResourceType":"AWS::EC2::VPC" }, - { "LogicalResourceId":"MySubnetOne", "ResourceType":"AWS::EC2::Subnet" }, - { "LogicalResourceId":"MySubnetTwo", "ResourceType":"AWS::EC2::Subnet" }, { "LogicalResourceId":"MyMskStreamProcessorMyMskEvent", "ResourceType":"AWS::Lambda::EventSourceMapping" } ] \ No newline at end of file diff --git a/integration/resources/expected/combination/function_with_msk_using_managed_policy.json b/integration/resources/expected/combination/function_with_msk_using_managed_policy.json index a257ccb24..04a09de04 100644 --- a/integration/resources/expected/combination/function_with_msk_using_managed_policy.json +++ b/integration/resources/expected/combination/function_with_msk_using_managed_policy.json @@ -2,8 +2,5 @@ { "LogicalResourceId":"MyMskStreamProcessor", "ResourceType":"AWS::Lambda::Function" }, { "LogicalResourceId":"MyMskStreamProcessorRole", "ResourceType":"AWS::IAM::Role" }, { "LogicalResourceId":"MyMskCluster", "ResourceType":"AWS::MSK::Cluster" }, - { "LogicalResourceId":"MyVpc", "ResourceType":"AWS::EC2::VPC" }, - { "LogicalResourceId":"MySubnetOne", "ResourceType":"AWS::EC2::Subnet" }, - { "LogicalResourceId":"MySubnetTwo", "ResourceType":"AWS::EC2::Subnet" }, { "LogicalResourceId":"MyMskStreamProcessorMyMskEvent", "ResourceType":"AWS::Lambda::EventSourceMapping" } ] \ No newline at end of file diff --git a/integration/resources/expected/single/basic_api_with_mode.json b/integration/resources/expected/single/basic_api_with_mode.json index fda128f59..3f83d75c4 100644 --- a/integration/resources/expected/single/basic_api_with_mode.json +++ b/integration/resources/expected/single/basic_api_with_mode.json @@ -1,6 +1,6 @@ [ {"LogicalResourceId": "MyApi", "ResourceType": "AWS::ApiGateway::RestApi"}, - {"LogicalResourceId": "MyApiDeploymenta808f15210", "ResourceType": "AWS::ApiGateway::Deployment"}, + {"LogicalResourceId": "MyApiDeployment", "ResourceType": "AWS::ApiGateway::Deployment"}, {"LogicalResourceId": "MyApiMyNewStageNameStage", "ResourceType": "AWS::ApiGateway::Stage"}, {"LogicalResourceId": "TestFunction", "ResourceType": "AWS::Lambda::Function"}, {"LogicalResourceId": "TestFunctionAliaslive", "ResourceType": "AWS::Lambda::Alias"}, diff --git a/integration/resources/templates/combination/all_policy_templates.yaml b/integration/resources/templates/combination/all_policy_templates.yaml index 0b3fa6c55..a781aaa8e 100644 --- a/integration/resources/templates/combination/all_policy_templates.yaml +++ b/integration/resources/templates/combination/all_policy_templates.yaml @@ -8,7 +8,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: hello.handler - Runtime: python2.7 + Runtime: python3.8 Policies: - SQSPollerPolicy: @@ -123,7 +123,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: hello.handler - Runtime: python2.7 + Runtime: python3.8 Policies: - SESEmailTemplateCrudPolicy: {} @@ -187,7 +187,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: hello.handler - Runtime: python2.7 + Runtime: python3.8 Policies: - ElasticMapReduceModifyInstanceFleetPolicy: ClusterId: name diff --git a/integration/resources/templates/combination/depends_on.yaml b/integration/resources/templates/combination/depends_on.yaml index 2aeb01c18..a3dba05bf 100644 --- a/integration/resources/templates/combination/depends_on.yaml +++ b/integration/resources/templates/combination/depends_on.yaml @@ -12,7 +12,7 @@ Resources: Role: "Fn::GetAtt": LambdaRole.Arn Handler: lambda_function.lambda_handler - Runtime: python2.7 + Runtime: python3.8 Timeout: 15 CodeUri: ${codeuri} diff --git a/integration/resources/templates/combination/function_with_all_event_types.yaml b/integration/resources/templates/combination/function_with_all_event_types.yaml index 5f96c3fb5..7e5cc56bf 100644 --- a/integration/resources/templates/combination/function_with_all_event_types.yaml +++ b/integration/resources/templates/combination/function_with_all_event_types.yaml @@ -1,4 +1,9 @@ AWSTemplateFormatVersion: '2010-09-09' + +Parameters: + ScheduleName: + Type: String + Conditions: MyCondition: Fn::Equals: @@ -39,14 +44,7 @@ Resources: Properties: Schedule: 'rate(1 minute)' Name: - Fn::Sub: - - TestSchedule${__StackName__} - - __StackName__: - Fn::Select: - - 3 - - Fn::Split: - - "-" - - Ref: AWS::StackName + Ref: ScheduleName Description: test schedule Enabled: False @@ -142,16 +140,3 @@ Resources: WriteCapacityUnits: 5 StreamSpecification: StreamViewType: NEW_IMAGE - -Outputs: - ScheduleName: - Description: "Name of the cw schedule" - Value: - Fn::Sub: - - TestSchedule${__StackName__} - - __StackName__: - Fn::Select: - - 3 - - Fn::Split: - - "-" - - Ref: AWS::StackName \ No newline at end of file diff --git a/integration/resources/templates/combination/function_with_custom_code_deploy.yaml b/integration/resources/templates/combination/function_with_custom_code_deploy.yaml index 73b3dfcfc..0be5b828e 100644 --- a/integration/resources/templates/combination/function_with_custom_code_deploy.yaml +++ b/integration/resources/templates/combination/function_with_custom_code_deploy.yaml @@ -5,7 +5,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: index.handler - Runtime: python2.7 + Runtime: python3.8 AutoPublishAlias: Live diff --git a/integration/resources/templates/combination/function_with_deployment_basic.yaml b/integration/resources/templates/combination/function_with_deployment_basic.yaml index 1d40b0087..b89c09ff3 100644 --- a/integration/resources/templates/combination/function_with_deployment_basic.yaml +++ b/integration/resources/templates/combination/function_with_deployment_basic.yaml @@ -5,7 +5,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: index.handler - Runtime: python2.7 + Runtime: python3.8 AutoPublishAlias: Live diff --git a/integration/resources/templates/combination/function_with_deployment_default_role_managed_policy.yaml b/integration/resources/templates/combination/function_with_deployment_default_role_managed_policy.yaml index 1a627a6a3..9d8e637c9 100644 --- a/integration/resources/templates/combination/function_with_deployment_default_role_managed_policy.yaml +++ b/integration/resources/templates/combination/function_with_deployment_default_role_managed_policy.yaml @@ -4,7 +4,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: index.handler - Runtime: python2.7 + Runtime: python3.8 AutoPublishAlias: Live DeploymentPreference: Type: Canary10Percent5Minutes diff --git a/integration/resources/templates/combination/function_with_deployment_globals.yaml b/integration/resources/templates/combination/function_with_deployment_globals.yaml index 99b280a02..03adf56ca 100644 --- a/integration/resources/templates/combination/function_with_deployment_globals.yaml +++ b/integration/resources/templates/combination/function_with_deployment_globals.yaml @@ -16,7 +16,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: index.handler - Runtime: python2.7 + Runtime: python3.8 AutoPublishAlias: Live DeploymentRole: diff --git a/integration/resources/templates/combination/function_with_mq.yaml b/integration/resources/templates/combination/function_with_mq.yaml index 703b6c696..2c3483dc1 100644 --- a/integration/resources/templates/combination/function_with_mq.yaml +++ b/integration/resources/templates/combination/function_with_mq.yaml @@ -12,56 +12,40 @@ Parameters: MinLength: 12 ConstraintDescription: The Amazon MQ broker password is required ! NoEcho: true + PreCreatedVpc: + Type: String + PreCreatedSubnetOne: + Type: String + MQBrokerUserSecretName: + Type: String + PreCreatedInternetGateway: + Type: String + MQBrokerName: + Description: The name of MQ Broker + Type: String + Default: TestMQBroker Resources: - MyVpc: - Type: AWS::EC2::VPC - Properties: - CidrBlock: "10.42.0.0/16" - DependsOn: - - MyLambdaExecutionRole - - InternetGateway: - Type: AWS::EC2::InternetGateway - - AttachGateway: - Type: AWS::EC2::VPCGatewayAttachment - Properties: - VpcId: - Ref: MyVpc - InternetGatewayId: - Ref: InternetGateway RouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: - Ref: MyVpc + Ref: PreCreatedVpc Route: Type: AWS::EC2::Route - DependsOn: AttachGateway Properties: RouteTableId: Ref: RouteTable DestinationCidrBlock: '0.0.0.0/0' GatewayId: - Ref: InternetGateway - PublicSubnet: - Type: AWS::EC2::Subnet - Properties: - VpcId: - Ref: MyVpc - CidrBlock: "10.42.0.0/24" - AvailabilityZone: - Fn::Select: - - 0 - - Fn::GetAZs: "" + Ref: PreCreatedInternetGateway PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: - Ref: PublicSubnet + Ref: PreCreatedSubnetOne RouteTableId: Ref: RouteTable @@ -71,7 +55,7 @@ Resources: GroupDescription: Limits security group ingress and egress traffic for the Amazon MQ instance VpcId: - Ref: MyVpc + Ref: PreCreatedVpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 8162 @@ -134,7 +118,8 @@ Resources: MyMqBroker: Properties: - BrokerName: TestMQBroker + BrokerName: + Ref: MQBrokerName DeploymentMode: SINGLE_INSTANCE EngineType: ACTIVEMQ EngineVersion: 5.15.12 @@ -147,7 +132,7 @@ Resources: SecurityGroups: - Ref: MQSecurityGroup SubnetIds: - - Ref: PublicSubnet + - Ref: PreCreatedSubnetOne Users: - ConsoleAccess: true Groups: @@ -157,6 +142,7 @@ Resources: Password: Ref: MQBrokerPassword Type: AWS::AmazonMQ::Broker + DependsOn: MyLambdaExecutionRole MyLambdaFunction: Type: AWS::Serverless::Function @@ -182,7 +168,8 @@ Resources: MQBrokerUserSecret: Type: AWS::SecretsManager::Secret Properties: - Name: MQBrokerUserPassword + Name: + Ref: MQBrokerUserSecretName SecretString: Fn::Sub: '{"username":"${MQBrokerUser}","password":"${MQBrokerPassword}"}' Description: SecretsManager Secret for broker user and password \ No newline at end of file diff --git a/integration/resources/templates/combination/function_with_mq_using_autogen_role.yaml b/integration/resources/templates/combination/function_with_mq_using_autogen_role.yaml index 962bee9d9..e48757b15 100644 --- a/integration/resources/templates/combination/function_with_mq_using_autogen_role.yaml +++ b/integration/resources/templates/combination/function_with_mq_using_autogen_role.yaml @@ -12,54 +12,40 @@ Parameters: MinLength: 12 ConstraintDescription: The Amazon MQ broker password is required ! NoEcho: true + PreCreatedVpc: + Type: String + PreCreatedSubnetTwo: + Type: String + MQBrokerUserSecretName2: + Type: String + PreCreatedInternetGateway: + Type: String + MQBrokerName2: + Description: The name of MQ Broker + Type: String + Default: TestMQBroker2 Resources: - MyVpc: - Type: AWS::EC2::VPC - Properties: - CidrBlock: "10.42.0.0/16" - - InternetGateway: - Type: AWS::EC2::InternetGateway - - AttachGateway: - Type: AWS::EC2::VPCGatewayAttachment - Properties: - VpcId: - Ref: MyVpc - InternetGatewayId: - Ref: InternetGateway RouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: - Ref: MyVpc + Ref: PreCreatedVpc Route: Type: AWS::EC2::Route - DependsOn: AttachGateway Properties: RouteTableId: Ref: RouteTable DestinationCidrBlock: '0.0.0.0/0' GatewayId: - Ref: InternetGateway - PublicSubnet: - Type: AWS::EC2::Subnet - Properties: - VpcId: - Ref: MyVpc - CidrBlock: "10.42.0.0/24" - AvailabilityZone: - Fn::Select: - - 0 - - Fn::GetAZs: "" + Ref: PreCreatedInternetGateway PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: - Ref: PublicSubnet + Ref: PreCreatedSubnetTwo RouteTableId: Ref: RouteTable @@ -69,7 +55,7 @@ Resources: GroupDescription: Limits security group ingress and egress traffic for the Amazon MQ instance VpcId: - Ref: MyVpc + Ref: PreCreatedVpc SecurityGroupIngress: - IpProtocol: tcp FromPort: 8162 @@ -94,7 +80,8 @@ Resources: MyMqBroker: Properties: - BrokerName: TestMQBroker2 + BrokerName: + Ref: MQBrokerName2 DeploymentMode: SINGLE_INSTANCE EngineType: ACTIVEMQ EngineVersion: 5.15.12 @@ -107,7 +94,7 @@ Resources: SecurityGroups: - Ref: MQSecurityGroup SubnetIds: - - Ref: PublicSubnet + - Ref: PreCreatedSubnetTwo Users: - ConsoleAccess: true Groups: @@ -140,7 +127,8 @@ Resources: MQBrokerUserSecret: Type: AWS::SecretsManager::Secret Properties: - Name: MQBrokerUserPassword2 + Name: + Ref: MQBrokerUserSecretName2 SecretString: Fn::Sub: '{"username":"${MQBrokerUser}","password":"${MQBrokerPassword}"}' Description: SecretsManager Secret for broker user and password diff --git a/integration/resources/templates/combination/function_with_msk.yaml b/integration/resources/templates/combination/function_with_msk.yaml index acaf3a383..9c4483e6c 100644 --- a/integration/resources/templates/combination/function_with_msk.yaml +++ b/integration/resources/templates/combination/function_with_msk.yaml @@ -1,37 +1,12 @@ -Resources: - MyVpc: - Type: "AWS::EC2::VPC" - Properties: - CidrBlock: "10.0.0.0/16" - DependsOn: - - MyLambdaExecutionRole - - MySubnetOne: - Type: "AWS::EC2::Subnet" - Properties: - VpcId: - Ref: MyVpc - CidrBlock: "10.0.0.0/24" - AvailabilityZone: - Fn::Select: - - 0 - - Fn::GetAZs: "" - DependsOn: - - MyVpc - - MySubnetTwo: - Type: "AWS::EC2::Subnet" - Properties: - VpcId: - Ref: MyVpc - CidrBlock: "10.0.1.0/24" - AvailabilityZone: - Fn::Select: - - 1 - - Fn::GetAZs: "" - DependsOn: - - MyVpc +Parameters: + PreCreatedSubnetOne: + Type: String + PreCreatedSubnetTwo: + Type: String + MskClusterName: + Type: String +Resources: MyLambdaExecutionRole: Type: AWS::IAM::Role Properties: @@ -68,20 +43,16 @@ Resources: Properties: BrokerNodeGroupInfo: ClientSubnets: - - Ref: MySubnetOne - - Ref: MySubnetTwo + - Ref: PreCreatedSubnetOne + - Ref: PreCreatedSubnetTwo InstanceType: kafka.t3.small StorageInfo: EBSStorageInfo: VolumeSize: 1 - ClusterName: MyMskClusterTestName + ClusterName: + Ref: MskClusterName KafkaVersion: 2.4.1.1 NumberOfBrokerNodes: 2 - DependsOn: - - MyVpc - - MySubnetOne - - MySubnetTwo - - MyLambdaExecutionRole MyMskStreamProcessor: Type: AWS::Serverless::Function @@ -96,14 +67,8 @@ Resources: Type: MSK Properties: StartingPosition: LATEST - Stream: + Stream: Ref: MyMskCluster Topics: - "MyDummyTestTopic" - DependsOn: - - MyVpc - - MySubnetOne - - MySubnetTwo - - MyLambdaExecutionRole - - MyMskCluster diff --git a/integration/resources/templates/combination/function_with_msk_using_managed_policy.yaml b/integration/resources/templates/combination/function_with_msk_using_managed_policy.yaml index 0fa07ba4a..8ee3b645d 100644 --- a/integration/resources/templates/combination/function_with_msk_using_managed_policy.yaml +++ b/integration/resources/templates/combination/function_with_msk_using_managed_policy.yaml @@ -1,53 +1,27 @@ -Resources: - MyVpc: - Type: "AWS::EC2::VPC" - Properties: - CidrBlock: "10.0.0.0/16" - - MySubnetOne: - Type: "AWS::EC2::Subnet" - Properties: - VpcId: - Ref: MyVpc - CidrBlock: "10.0.0.0/24" - AvailabilityZone: - Fn::Select: - - 0 - - Fn::GetAZs: "" - DependsOn: - - MyVpc - - MySubnetTwo: - Type: "AWS::EC2::Subnet" - Properties: - VpcId: - Ref: MyVpc - CidrBlock: "10.0.1.0/24" - AvailabilityZone: - Fn::Select: - - 1 - - Fn::GetAZs: "" - DependsOn: - - MyVpc +Parameters: + PreCreatedSubnetOne: + Type: String + PreCreatedSubnetTwo: + Type: String + MskClusterName2: + Type: String +Resources: MyMskCluster: Type: 'AWS::MSK::Cluster' Properties: BrokerNodeGroupInfo: ClientSubnets: - - Ref: MySubnetOne - - Ref: MySubnetTwo + - Ref: PreCreatedSubnetOne + - Ref: PreCreatedSubnetTwo InstanceType: kafka.t3.small StorageInfo: EBSStorageInfo: VolumeSize: 1 - ClusterName: MyMskClusterTestName2 + ClusterName: + Ref: MskClusterName2 KafkaVersion: 2.4.1.1 NumberOfBrokerNodes: 2 - DependsOn: - - MyVpc - - MySubnetOne - - MySubnetTwo MyMskStreamProcessor: Type: AWS::Serverless::Function @@ -60,13 +34,8 @@ Resources: Type: MSK Properties: StartingPosition: LATEST - Stream: + Stream: Ref: MyMskCluster Topics: - "MyDummyTestTopic" - DependsOn: - - MyVpc - - MySubnetOne - - MySubnetTwo - - MyMskCluster diff --git a/integration/resources/templates/combination/function_with_policy_templates.yaml b/integration/resources/templates/combination/function_with_policy_templates.yaml index bd0cec1c0..31bcc1a30 100644 --- a/integration/resources/templates/combination/function_with_policy_templates.yaml +++ b/integration/resources/templates/combination/function_with_policy_templates.yaml @@ -14,7 +14,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: hello.handler - Runtime: python2.7 + Runtime: python3.8 Policies: - SQSPollerPolicy: QueueName: diff --git a/integration/resources/templates/combination/function_with_resource_refs.yaml b/integration/resources/templates/combination/function_with_resource_refs.yaml index 1d091fe91..d8b92b71b 100644 --- a/integration/resources/templates/combination/function_with_resource_refs.yaml +++ b/integration/resources/templates/combination/function_with_resource_refs.yaml @@ -10,14 +10,14 @@ Resources: Properties: CodeUri: ${codeuri} Handler: hello.handler - Runtime: python2.7 + Runtime: python3.8 AutoPublishAlias: Live MyOtherFunction: Type: 'AWS::Serverless::Function' Properties: CodeUri: ${codeuri} - Runtime: python2.7 + Runtime: python3.8 Handler: hello.handler Environment: Variables: diff --git a/integration/resources/templates/combination/function_with_schedule.yaml b/integration/resources/templates/combination/function_with_schedule.yaml index 97cda940b..42e352db8 100644 --- a/integration/resources/templates/combination/function_with_schedule.yaml +++ b/integration/resources/templates/combination/function_with_schedule.yaml @@ -1,3 +1,7 @@ +Parameters: + ScheduleName: + Type: String + Resources: MyLambdaFunction: Type: AWS::Serverless::Function @@ -13,25 +17,6 @@ Resources: Schedule: 'rate(5 minutes)' Input: '{"Hello": "world!"}' Name: - Fn::Sub: - - TestSchedule${__StackName__} - - __StackName__: - Fn::Select: - - 3 - - Fn::Split: - - "-" - - Ref: AWS::StackName + Ref: ScheduleName Description: test schedule Enabled: True -Outputs: - ScheduleName: - Description: "Name of the cw schedule" - Value: - Fn::Sub: - - TestSchedule${__StackName__} - - __StackName__: - Fn::Select: - - 3 - - Fn::Split: - - "-" - - Ref: AWS::StackName \ No newline at end of file diff --git a/integration/resources/templates/combination/state_machine_with_policy_templates.yaml b/integration/resources/templates/combination/state_machine_with_policy_templates.yaml index 5d1ed150d..cafa9f007 100644 --- a/integration/resources/templates/combination/state_machine_with_policy_templates.yaml +++ b/integration/resources/templates/combination/state_machine_with_policy_templates.yaml @@ -24,7 +24,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: hello.handler - Runtime: python2.7 + Runtime: python3.8 MyQueue: Type: AWS::SQS::Queue diff --git a/integration/resources/templates/single/basic_application_sar_location.yaml b/integration/resources/templates/single/basic_application_sar_location.yaml index 5bd59c2e4..254eca341 100644 --- a/integration/resources/templates/single/basic_application_sar_location.yaml +++ b/integration/resources/templates/single/basic_application_sar_location.yaml @@ -3,7 +3,7 @@ Resources: Type: AWS::Serverless::Application Properties: Location: - ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 SemanticVersion: 1.0.2 Parameters: IdentityNameParameter: test diff --git a/integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml b/integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml index 471af7ae0..3de30267d 100644 --- a/integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml +++ b/integration/resources/templates/single/basic_application_sar_location_with_intrinsics.yaml @@ -6,7 +6,7 @@ Parameters: Mappings: SARApplication: us-east-1: - ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python + ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 us-east-2: ApplicationId: arn:aws:serverlessrepo:us-east-1:077246666028:applications/hello-world-python3 us-west-1: diff --git a/integration/resources/templates/single/basic_function_event_destinations.yaml b/integration/resources/templates/single/basic_function_event_destinations.yaml index 2129313b1..d3f8d9934 100644 --- a/integration/resources/templates/single/basic_function_event_destinations.yaml +++ b/integration/resources/templates/single/basic_function_event_destinations.yaml @@ -42,7 +42,7 @@ Resources: } }; Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs14.x MemorySize: 1024 MyTestFunction2: Type: AWS::Serverless::Function @@ -74,7 +74,7 @@ Resources: } }; Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs14.x MemorySize: 1024 DestinationLambda: Type: AWS::Serverless::Function @@ -88,7 +88,7 @@ Resources: return response; }; Handler: index.handler - Runtime: nodejs10.x + Runtime: nodejs14.x MemorySize: 1024 DestinationSQS: Condition: QueueCreationDisabled diff --git a/integration/resources/templates/single/basic_layer_with_parameters.yaml b/integration/resources/templates/single/basic_layer_with_parameters.yaml index f5eb8fdb8..e347b96e1 100644 --- a/integration/resources/templates/single/basic_layer_with_parameters.yaml +++ b/integration/resources/templates/single/basic_layer_with_parameters.yaml @@ -7,7 +7,7 @@ Parameters: Default: MIT-0 Runtimes: Type: CommaDelimitedList - Default: nodejs12.x,nodejs10.x + Default: nodejs12.x,nodejs14.x LayerName: Type: String Default: MyNamedLayerVersion diff --git a/integration/resources/templates/single/function_with_deployment_preference_alarms_intrinsic_if.yaml b/integration/resources/templates/single/function_with_deployment_preference_alarms_intrinsic_if.yaml index 261571ff5..ddc1a13bd 100644 --- a/integration/resources/templates/single/function_with_deployment_preference_alarms_intrinsic_if.yaml +++ b/integration/resources/templates/single/function_with_deployment_preference_alarms_intrinsic_if.yaml @@ -9,7 +9,7 @@ Resources: Properties: CodeUri: ${codeuri} Handler: hello.handler - Runtime: python2.7 + Runtime: python3.8 AutoPublishAlias: live DeploymentPreference: Type: Linear10PercentEvery3Minutes diff --git a/integration/setup/__init__.py b/integration/setup/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/integration/setup/companion-stack.yaml b/integration/setup/companion-stack.yaml new file mode 100644 index 000000000..35521d7f2 --- /dev/null +++ b/integration/setup/companion-stack.yaml @@ -0,0 +1,60 @@ +Resources: + PreCreatedVpc: + Type: "AWS::EC2::VPC" + Properties: + CidrBlock: "10.0.0.0/16" + + PreCreatedSubnetOne: + Type: "AWS::EC2::Subnet" + Properties: + VpcId: + Ref: PreCreatedVpc + CidrBlock: "10.0.0.0/24" + AvailabilityZone: + Fn::Select: + - 0 + - Fn::GetAZs: "" + + PreCreatedSubnetTwo: + Type: "AWS::EC2::Subnet" + Properties: + VpcId: + Ref: PreCreatedVpc + CidrBlock: "10.0.1.0/24" + AvailabilityZone: + Fn::Select: + - 1 + - Fn::GetAZs: "" + + PreCreatedInternetGateway: + Type: AWS::EC2::InternetGateway + + PreCreatedAttachGateway: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: + Ref: PreCreatedVpc + InternetGatewayId: + Ref: PreCreatedInternetGateway + +Outputs: + PreCreatedVpc: + Description: "Pre-created VPC that can be used inside other tests" + Value: + Ref: PreCreatedVpc + PreCreatedSubnetTwo: + Description: "Pre-created #2 subnet that can be used inside other tests" + Value: + Ref: PreCreatedSubnetTwo + PreCreatedSubnetOne: + Description: "Pre-created #1 subnet that can be used inside other tests" + Value: + Ref: PreCreatedSubnetOne + PreCreatedInternetGateway: + Description: "Pre-created Internet Gateway that can be used inside other tests" + Value: + Ref: PreCreatedInternetGateway + PreCreatedAttachGateway: + Description: "Pre-created Attach Gateway that can be used inside other tests" + Value: + Ref: PreCreatedAttachGateway \ No newline at end of file diff --git a/integration/setup/test_setup_teardown.py b/integration/setup/test_setup_teardown.py new file mode 100644 index 000000000..49ba6aa96 --- /dev/null +++ b/integration/setup/test_setup_teardown.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.mark.setup +def test_setup(setup_companion_stack_once): + assert True + + +@pytest.mark.teardown +def test_teardown(delete_companion_stack_once): + assert True diff --git a/integration/single/test_basic_api.py b/integration/single/test_basic_api.py index 846d96718..e8de40556 100644 --- a/integration/single/test_basic_api.py +++ b/integration/single/test_basic_api.py @@ -1,6 +1,12 @@ +import time +from unittest.case import skipIf + from integration.helpers.base_test import BaseTest import requests +from integration.helpers.resource import current_region_does_not_support +from integration.config.service_names import MODE + class TestBasicApi(BaseTest): """ @@ -25,6 +31,7 @@ def test_basic_api(self): self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0) + @skipIf(current_region_does_not_support([MODE]), "Mode is not supported in this testing region") def test_basic_api_with_mode(self): """ Creates an API and updates its DefinitionUri @@ -39,8 +46,16 @@ def test_basic_api_with_mode(self): # Removes get from the API self.update_and_verify_stack("single/basic_api_with_mode_update") - response = requests.get(f"{api_endpoint}/get") + # API Gateway by default returns 403 if a path do not exist + retries = 20 + while retries > 0: + retries -= 1 + response = requests.get(f"{api_endpoint}/get") + if response.status_code != 500: + break + time.sleep(5) + self.assertEqual(response.status_code, 403) def test_basic_api_inline_openapi(self): diff --git a/integration/single/test_basic_application.py b/integration/single/test_basic_application.py index 15ae0861f..afc7a97c5 100644 --- a/integration/single/test_basic_application.py +++ b/integration/single/test_basic_application.py @@ -38,7 +38,7 @@ def test_basic_application_sar_location(self): functions = self.get_stack_resources("AWS::Lambda::Function", nested_stack_resource) self.assertEqual(len(functions), 1) - self.assertEqual(functions[0]["LogicalResourceId"], "helloworldpython") + self.assertEqual(functions[0]["LogicalResourceId"], "helloworldpython3") @skipIf( current_region_does_not_support(["ServerlessRepo"]), "ServerlessRepo is not supported in this testing region" @@ -47,7 +47,7 @@ def test_basic_application_sar_location_with_intrinsics(self): """ Creates an application with a lambda function with intrinsics """ - expected_function_name = "helloworldpython" if self.get_region() == "us-east-1" else "helloworldpython3" + expected_function_name = "helloworldpython3" self.create_and_verify_stack("single/basic_application_sar_location_with_intrinsics") nested_stack_resource = self.get_stack_nested_stack_resources() From d16cd218d503b51dcccd0d3bc6f491e22ce29d85 Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Mon, 6 Dec 2021 15:25:28 -0500 Subject: [PATCH 17/59] fix: Check type of resource type is a string (#2252) * Check type of resource type is a string * Condense logic * Black reformat --- samtranslator/model/__init__.py | 2 +- .../input/resource_with_invalid_type.yaml | 14 ++++ .../aws-cn/resource_with_invalid_type.json | 67 +++++++++++++++++++ .../resource_with_invalid_type.json | 67 +++++++++++++++++++ .../output/resource_with_invalid_type.json | 67 +++++++++++++++++++ tests/translator/test_translator.py | 1 + 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 tests/translator/input/resource_with_invalid_type.yaml create mode 100644 tests/translator/output/aws-cn/resource_with_invalid_type.json create mode 100644 tests/translator/output/aws-us-gov/resource_with_invalid_type.json create mode 100644 tests/translator/output/resource_with_invalid_type.json diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index d535fb142..892f62e2b 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -489,7 +489,7 @@ def __init__(self, *modules): self.resource_types[resource_class.resource_type] = resource_class def can_resolve(self, resource_dict): - if not isinstance(resource_dict, dict) or "Type" not in resource_dict: + if not isinstance(resource_dict, dict) or not isinstance(resource_dict.get("Type"), string_types): return False return resource_dict["Type"] in self.resource_types diff --git a/tests/translator/input/resource_with_invalid_type.yaml b/tests/translator/input/resource_with_invalid_type.yaml new file mode 100644 index 000000000..46ace5461 --- /dev/null +++ b/tests/translator/input/resource_with_invalid_type.yaml @@ -0,0 +1,14 @@ +Resources: + FunctionInvalid: + Type: + AWS::Serverless::Function: invalid_field + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs12.x + FunctionValid: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs12.x diff --git a/tests/translator/output/aws-cn/resource_with_invalid_type.json b/tests/translator/output/aws-cn/resource_with_invalid_type.json new file mode 100644 index 000000000..bde97009d --- /dev/null +++ b/tests/translator/output/aws-cn/resource_with_invalid_type.json @@ -0,0 +1,67 @@ +{ + "Resources": { + "FunctionInvalid": { + "Type": { + "AWS::Serverless::Function": "invalid_field" + }, + "Properties": { + "CodeUri": "s3://sam-demo-bucket/member_portal.zip", + "Handler": "index.gethtml", + "Runtime": "nodejs12.x" + } + }, + "FunctionValid": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "FunctionValidRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "FunctionValidRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/resource_with_invalid_type.json b/tests/translator/output/aws-us-gov/resource_with_invalid_type.json new file mode 100644 index 000000000..793a99261 --- /dev/null +++ b/tests/translator/output/aws-us-gov/resource_with_invalid_type.json @@ -0,0 +1,67 @@ +{ + "Resources": { + "FunctionInvalid": { + "Type": { + "AWS::Serverless::Function": "invalid_field" + }, + "Properties": { + "CodeUri": "s3://sam-demo-bucket/member_portal.zip", + "Handler": "index.gethtml", + "Runtime": "nodejs12.x" + } + }, + "FunctionValid": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "FunctionValidRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "FunctionValidRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} diff --git a/tests/translator/output/resource_with_invalid_type.json b/tests/translator/output/resource_with_invalid_type.json new file mode 100644 index 000000000..92cca3173 --- /dev/null +++ b/tests/translator/output/resource_with_invalid_type.json @@ -0,0 +1,67 @@ +{ + "Resources": { + "FunctionInvalid": { + "Type": { + "AWS::Serverless::Function": "invalid_field" + }, + "Properties": { + "CodeUri": "s3://sam-demo-bucket/member_portal.zip", + "Handler": "index.gethtml", + "Runtime": "nodejs12.x" + } + }, + "FunctionValid": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "FunctionValidRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "FunctionValidRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index ea31f443e..d26398a37 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -282,6 +282,7 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): "function_with_mq_virtual_host", "simpletable", "simpletable_with_sse", + "resource_with_invalid_type", "implicit_api", "explicit_api", "api_description", From 6daf70684076eeeb6c2b82f249742c1d426e08ae Mon Sep 17 00:00:00 2001 From: marekaiv <85357404+marekaiv@users.noreply.github.com> Date: Mon, 13 Dec 2021 20:55:41 -0500 Subject: [PATCH 18/59] Do not abort SAR loop on throttling (#2240) * Do not abort SAR loop on throttling * Address code review comments * Simplify SAR polling loop and add test coverage --- .../application/serverless_app_plugin.py | 102 ++++++++++-------- .../application/test_serverless_app_plugin.py | 79 +++++++++++++- 2 files changed, 132 insertions(+), 49 deletions(-) diff --git a/samtranslator/plugins/application/serverless_app_plugin.py b/samtranslator/plugins/application/serverless_app_plugin.py index d9e5cf513..3b6e6246c 100644 --- a/samtranslator/plugins/application/serverless_app_plugin.py +++ b/samtranslator/plugins/application/serverless_app_plugin.py @@ -195,6 +195,7 @@ def _handle_create_cfn_template_request(self, app_id, semver, key, logical_id): ApplicationId=self._sanitize_sar_str_param(app_id), SemanticVersion=self._sanitize_sar_str_param(semver) ) response = self._sar_service_call(create_cfn_template, logical_id, app_id, semver) + LOG.info("Requested to create CFN template {}/{} in serverless application repo.".format(app_id, semver)) self._applications[key] = response[self.TEMPLATE_URL_KEY] if response["Status"] != "ACTIVE": @@ -303,57 +304,73 @@ def on_after_transform_template(self, template): :param dict template: Dictionary of the SAM template :return: Nothing """ - if self._wait_for_template_active_status and not self._validate_only: - start_time = time() - while (time() - start_time) < self.TEMPLATE_WAIT_TIMEOUT_SECONDS: - temp = self._in_progress_templates - self._in_progress_templates = [] - - # Check each resource to make sure it's active - LOG.info("Checking resources in serverless application repo...") - for application_id, template_id in temp: - get_cfn_template = ( - lambda application_id, template_id: self._sar_client.get_cloud_formation_template( - ApplicationId=self._sanitize_sar_str_param(application_id), - TemplateId=self._sanitize_sar_str_param(template_id), - ) - ) - response = self._sar_service_call(get_cfn_template, application_id, application_id, template_id) - self._handle_get_cfn_template_response(response, application_id, template_id) - LOG.info("Finished checking resources in serverless application repo.") + if not self._wait_for_template_active_status or self._validate_only: + return - # Don't sleep if there are no more templates with PREPARING status - if len(self._in_progress_templates) == 0: - break + start_time = time() + while (time() - start_time) < self.TEMPLATE_WAIT_TIMEOUT_SECONDS: + # Check each resource to make sure it's active + LOG.info("Checking resources in serverless application repo...") + idx = 0 + while idx < len(self._in_progress_templates): + application_id, template_id = self._in_progress_templates[idx] + get_cfn_template = lambda application_id, template_id: self._sar_client.get_cloud_formation_template( + ApplicationId=self._sanitize_sar_str_param(application_id), + TemplateId=self._sanitize_sar_str_param(template_id), + ) - # Sleep a little so we don't spam service calls - sleep(self.SLEEP_TIME_SECONDS) + try: + response = self._sar_service_call(get_cfn_template, application_id, application_id, template_id) + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "TooManyRequestsException": + LOG.debug("SAR call timed out for application id {}".format(application_id)) + break # We were throttled by SAR, break out to a sleep + else: + raise e + + if self._is_template_active(response, application_id, template_id): + self._in_progress_templates.remove((application_id, template_id)) + else: + idx += 1 # check next template + + LOG.info("Finished checking resources in serverless application repo.") + + # Don't sleep if there are no more templates with PREPARING status + if len(self._in_progress_templates) == 0: + break + + # Sleep a little so we don't spam service calls + sleep(self._get_sleep_time_sec()) + + # Not all templates reached active status + if len(self._in_progress_templates) != 0: + application_ids = [items[0] for items in self._in_progress_templates] + raise InvalidResourceException( + application_ids, "Timed out waiting for nested stack templates " "to reach ACTIVE status." + ) - # Not all templates reached active status - if len(self._in_progress_templates) != 0: - application_ids = [items[0] for items in self._in_progress_templates] - raise InvalidResourceException( - application_ids, "Timed out waiting for nested stack templates " "to reach ACTIVE status." - ) + def _get_sleep_time_sec(self): + return self.SLEEP_TIME_SECONDS - def _handle_get_cfn_template_response(self, response, application_id, template_id): + def _is_template_active(self, response, application_id, template_id): """ - Handles the response from the SAR service call + Checks the response from a SAR service call; returns True if the template is active, + throws an exception if the request expired and returns False in all other cases. :param dict response: the response dictionary from the app repo :param string application_id: the ApplicationId :param string template_id: the unique TemplateId for this application """ - status = response["Status"] - if status != "ACTIVE": - # Other options are PREPARING and EXPIRED. - if status == "EXPIRED": - message = ( - "Template for {} with id {} returned status: {}. Cannot access an expired " - "template.".format(application_id, template_id, status) - ) - raise InvalidResourceException(application_id, message) - self._in_progress_templates.append((application_id, template_id)) + status = response["Status"] # options: PREPARING, EXPIRED or ACTIVE + + if status == "EXPIRED": + message = "Template for {} with id {} returned status: {}. Cannot access an expired " "template.".format( + application_id, template_id, status + ) + raise InvalidResourceException(application_id, message) + + return status == "ACTIVE" @cw_timer(prefix="External", name="SAR") def _sar_service_call(self, service_call_lambda, logical_id, *args): @@ -372,9 +389,6 @@ def _sar_service_call(self, service_call_lambda, logical_id, *args): error_code = e.response["Error"]["Code"] if error_code in ("AccessDeniedException", "NotFoundException"): raise InvalidResourceException(logical_id, e.response["Error"]["Message"]) - - # 'ForbiddenException'- SAR rejects connection - LOG.exception(e) raise e def _resource_is_supported(self, resource_type): diff --git a/tests/plugins/application/test_serverless_app_plugin.py b/tests/plugins/application/test_serverless_app_plugin.py index b27d0fb2e..c130bb76b 100644 --- a/tests/plugins/application/test_serverless_app_plugin.py +++ b/tests/plugins/application/test_serverless_app_plugin.py @@ -1,5 +1,6 @@ import boto3 import itertools +from botocore.exceptions import ClientError from mock import Mock, patch from unittest import TestCase @@ -7,6 +8,7 @@ from samtranslator.plugins.application.serverless_app_plugin import ServerlessAppPlugin from samtranslator.plugins.exceptions import InvalidPluginException +from samtranslator.model.exceptions import InvalidResourceException # TODO: run tests when AWS CLI is not configured (so they can run in brazil) @@ -253,9 +255,76 @@ def __init__(self, app_id="app_id", semver="1.3.5"): # self.plugin.on_before_transform_resource(app_resources[0][0], 'AWS::Serverless::Application', app_resources[0][1].properties) -# class TestServerlessAppPlugin_on_after_transform_template(TestCase): -# def setUp(self): -# self.plugin = SeverlessAppPlugin() - -# # TODO: test this lifecycle event +class TestServerlessAppPlugin_on_after_transform_template(TestCase): + def test_sar_throttling_doesnt_stop_processing(self): + client = Mock() + client.get_cloud_formation_template = Mock() + client.get_cloud_formation_template.side_effect = ClientError( + {"Error": {"Code": "TooManyRequestsException"}}, "GetCloudFormationTemplate" + ) + plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False) + plugin._get_sleep_time_sec = Mock() + plugin._get_sleep_time_sec.return_value = 0.02 + plugin._in_progress_templates = [("appid", "template"), ("appid2", "template2")] + plugin.TEMPLATE_WAIT_TIMEOUT_SECONDS = 0.2 + with self.assertRaises(InvalidResourceException): + plugin.on_after_transform_template("template") + # confirm we had at least two attempts to call SAR and that we executed a sleep + self.assertGreater(client.get_cloud_formation_template.call_count, 1) + self.assertGreaterEqual(plugin._get_sleep_time_sec.call_count, 1) + + def test_unexpected_sar_error_stops_processing(self): + client = Mock() + client.get_cloud_formation_template = Mock() + client.get_cloud_formation_template.side_effect = ClientError( + {"Error": {"Code": "BadBadError"}}, "GetCloudFormationTemplate" + ) + plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False) + plugin._in_progress_templates = [("appid", "template")] + with self.assertRaises(ClientError): + plugin.on_after_transform_template("template") + + def test_sar_success_one_app(self): + client = Mock() + client.get_cloud_formation_template = Mock() + client.get_cloud_formation_template.return_value = {"Status": STATUS_ACTIVE} + plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False) + plugin._in_progress_templates = [("appid", "template")] + plugin.on_after_transform_template("template") + # should have exactly one call to SAR + self.assertEqual(client.get_cloud_formation_template.call_count, 1) + + def test_sar_success_two_apps(self): + client = Mock() + client.get_cloud_formation_template = Mock() + client.get_cloud_formation_template.return_value = {"Status": STATUS_ACTIVE} + plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False) + plugin._in_progress_templates = [("appid1", "template1"), ("appid2", "template2")] + plugin.on_after_transform_template("template") + # should have exactly one call to SAR per app + self.assertEqual(client.get_cloud_formation_template.call_count, 2) + + def test_expired_sar_app_throws(self): + client = Mock() + client.get_cloud_formation_template = Mock() + client.get_cloud_formation_template.return_value = {"Status": STATUS_EXPIRED} + plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False) + plugin._in_progress_templates = [("appid1", "template1"), ("appid2", "template2")] + with self.assertRaises(InvalidResourceException): + plugin.on_after_transform_template("template") + # should have exactly one call to SAR since the first app will be expired + self.assertEqual(client.get_cloud_formation_template.call_count, 1) + + def test_sleep_between_sar_checks(self): + client = Mock() + client.get_cloud_formation_template = Mock() + client.get_cloud_formation_template.side_effect = [{"Status": STATUS_PREPARING}, {"Status": STATUS_ACTIVE}] + plugin = ServerlessAppPlugin(sar_client=client, wait_for_template_active_status=True, validate_only=False) + plugin._in_progress_templates = [("appid1", "template1")] + plugin._get_sleep_time_sec = Mock() + plugin._get_sleep_time_sec.return_value = 0.001 + plugin.on_after_transform_template("template") + # should have exactly two calls to SAR + self.assertEqual(client.get_cloud_formation_template.call_count, 2) + self.assertEqual(plugin._get_sleep_time_sec.call_count, 1) # make sure we slept once From 3808cc4f965db0287522935209af3520d8db6063 Mon Sep 17 00:00:00 2001 From: marekaiv <85357404+marekaiv@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:55:59 -0500 Subject: [PATCH 19/59] Improve error message for an invalid ResourcePolicy element (#2271) --- samtranslator/model/api/api_generator.py | 3 +++ ...or_resource_policy_not_dict_empty_api.yaml | 25 +++++++++++++++++++ .../error_resource_policy_not_dict.json | 2 +- ...or_resource_policy_not_dict_empty_api.json | 3 +++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/translator/input/error_resource_policy_not_dict_empty_api.yaml create mode 100644 tests/translator/output/error_resource_policy_not_dict_empty_api.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index b79885023..f6587e9d8 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -702,6 +702,9 @@ def _add_auth(self): self._set_default_apikey_required(swagger_editor) if auth_properties.ResourcePolicy: + SwaggerEditor.validate_is_dict( + auth_properties.ResourcePolicy, "ResourcePolicy must be a map (ResourcePolicyStatement)." + ) for path in swagger_editor.iter_on_path(): swagger_editor.add_resource_policy(auth_properties.ResourcePolicy, path, self.stage_name) if auth_properties.ResourcePolicy.get("CustomStatements"): diff --git a/tests/translator/input/error_resource_policy_not_dict_empty_api.yaml b/tests/translator/input/error_resource_policy_not_dict_empty_api.yaml new file mode 100644 index 000000000..d81711c20 --- /dev/null +++ b/tests/translator/input/error_resource_policy_not_dict_empty_api.yaml @@ -0,0 +1,25 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Bad, bad resource policy +Resources: + APIGatewayVpcEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + SubnetIds: + - subnet-01234567 + - subnet-12345678 + SecurityGroupIds: + - sg-0a0a0a0a0a0a0a0a0 + ServiceName: com.amazonaws.eu-west-1.execute-api + VpcId: vpc-0a0a0a0a + VpcEndpointType: Interface + PrivateDnsEnabled: false + ServerlessApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + EndpointConfiguration: + Type: PRIVATE + VPCEndpointIds: + - Ref: APIGatewayVpcEndpoint + Auth: + ResourcePolicy: IntrinsicVpceWhitelist:! Ref APIGatewayVpcEndpoint diff --git a/tests/translator/output/error_resource_policy_not_dict.json b/tests/translator/output/error_resource_policy_not_dict.json index 7595d16c4..5ed2d9c8a 100644 --- a/tests/translator/output/error_resource_policy_not_dict.json +++ b/tests/translator/output/error_resource_policy_not_dict.json @@ -1,3 +1,3 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Resource Policy is not a valid dictionary." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. ResourcePolicy must be a map (ResourcePolicyStatement)." } \ No newline at end of file diff --git a/tests/translator/output/error_resource_policy_not_dict_empty_api.json b/tests/translator/output/error_resource_policy_not_dict_empty_api.json new file mode 100644 index 000000000..5ed2d9c8a --- /dev/null +++ b/tests/translator/output/error_resource_policy_not_dict_empty_api.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. ResourcePolicy must be a map (ResourcePolicyStatement)." +} \ No newline at end of file From 8b895756925dd9cddb837bec8498cd6788c78fdd Mon Sep 17 00:00:00 2001 From: sattigar <67429403+sattigar@users.noreply.github.com> Date: Tue, 21 Dec 2021 13:14:53 -0800 Subject: [PATCH 20/59] =?UTF-8?q?Fixing=20DisableExecuteApiEndpoint=20prop?= =?UTF-8?q?erty=20for=20REST=20and=20adding=20integ=20t=E2=80=A6=20(#2272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixing DisableExecuteApiEndpoint property for REST and adding integ tests. * Fixing DisableExecuteApiEndpoint property for REST and adding integ tests. * Fixing output templates to match py2 hashing. --- samtranslator/model/api/api_generator.py | 6 +- ...api_with_disable_api_execute_endpoint.yaml | 22 +++ ...api_with_disable_api_execute_endpoint.json | 130 +++++++++++++++++ ...api_with_disable_api_execute_endpoint.json | 138 ++++++++++++++++++ ...api_with_disable_api_execute_endpoint.json | 138 ++++++++++++++++++ tests/translator/test_translator.py | 1 + 6 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 tests/translator/input/api_with_disable_api_execute_endpoint.yaml create mode 100644 tests/translator/output/api_with_disable_api_execute_endpoint.json create mode 100644 tests/translator/output/aws-cn/api_with_disable_api_execute_endpoint.json create mode 100644 tests/translator/output/aws-us-gov/api_with_disable_api_execute_endpoint.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index f6587e9d8..0fab23e58 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -276,6 +276,9 @@ def _construct_rest_api(self): self._add_binary_media_types() self._add_models() + if self.disable_execute_api_endpoint is not None: + self._add_endpoint_extension() + if self.definition_uri: rest_api.BodyS3Location = self._construct_body_s3_dict() elif self.definition_body: @@ -292,9 +295,6 @@ def _construct_rest_api(self): if self.mode: rest_api.Mode = self.mode - if self.disable_execute_api_endpoint is not None: - self._add_endpoint_extension() - return rest_api def _add_endpoint_extension(self): diff --git a/tests/translator/input/api_with_disable_api_execute_endpoint.yaml b/tests/translator/input/api_with_disable_api_execute_endpoint.yaml new file mode 100644 index 000000000..7be8dc3e5 --- /dev/null +++ b/tests/translator/input/api_with_disable_api_execute_endpoint.yaml @@ -0,0 +1,22 @@ +Resources: + ApiGatewayApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + DisableExecuteApiEndpoint: True + ApiFunction: # Adds a GET api endpoint at "/" to the ApiGatewayApi via an Api event + Type: AWS::Serverless::Function + Properties: + Events: + ApiEvent: + Type: Api + Properties: + Path: / + Method: get + RestApiId: + Ref: ApiGatewayApi + Runtime: python3.7 + Handler: index.handler + InlineCode: | + def handler(event, context): + return {'body': 'Hello World!', 'statusCode': 200} \ No newline at end of file diff --git a/tests/translator/output/api_with_disable_api_execute_endpoint.json b/tests/translator/output/api_with_disable_api_execute_endpoint.json new file mode 100644 index 000000000..d2d3c5857 --- /dev/null +++ b/tests/translator/output/api_with_disable_api_execute_endpoint.json @@ -0,0 +1,130 @@ +{ + "Resources": { + "ApiGatewayApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0", + "x-amazon-apigateway-endpoint-configuration": { + "disableExecuteApiEndpoint": true + } + } + } + }, + "ApiGatewayApiDeploymenta13ba42368": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ApiGatewayApi" + }, + "Description": "RestApi deployment id: a13ba42368cb482635c1d715f5e0199e94d79222", + "StageName": "Stage" + } + }, + "ApiFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ApiFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "def handler(event, context):\n return {'body': 'Hello World!', 'statusCode': 200}" + }, + "Role": { + "Fn::GetAtt": [ + "ApiFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ApiFunctionApiEventPermissionprod": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ApiGatewayApi" + } + } + ] + } + } + }, + "ApiGatewayApiprodStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ApiGatewayApiDeploymenta13ba42368" + }, + "RestApiId": { + "Ref": "ApiGatewayApi" + }, + "StageName": "prod" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_with_disable_api_execute_endpoint.json b/tests/translator/output/aws-cn/api_with_disable_api_execute_endpoint.json new file mode 100644 index 000000000..7b13a036c --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_disable_api_execute_endpoint.json @@ -0,0 +1,138 @@ +{ + "Resources": { + "ApiGatewayApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0", + "x-amazon-apigateway-endpoint-configuration": { + "disableExecuteApiEndpoint": true + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "ApiGatewayApiDeployment31ce724e39": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ApiGatewayApi" + }, + "Description": "RestApi deployment id: 31ce724e3998cb16b80266cfb4868670a87430bf", + "StageName": "Stage" + } + }, + "ApiFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ApiFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "def handler(event, context):\n return {'body': 'Hello World!', 'statusCode': 200}" + }, + "Role": { + "Fn::GetAtt": [ + "ApiFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ApiFunctionApiEventPermissionprod": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ApiGatewayApi" + } + } + ] + } + } + }, + "ApiGatewayApiprodStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ApiGatewayApiDeployment31ce724e39" + }, + "RestApiId": { + "Ref": "ApiGatewayApi" + }, + "StageName": "prod" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_with_disable_api_execute_endpoint.json b/tests/translator/output/aws-us-gov/api_with_disable_api_execute_endpoint.json new file mode 100644 index 000000000..a044be47c --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_disable_api_execute_endpoint.json @@ -0,0 +1,138 @@ +{ + "Resources": { + "ApiGatewayApiDeployment88816bd125": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ApiGatewayApi" + }, + "Description": "RestApi deployment id: 88816bd1258061ab1a1138fb2723a76f221c5111", + "StageName": "Stage" + } + }, + "ApiGatewayApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0", + "x-amazon-apigateway-endpoint-configuration": { + "disableExecuteApiEndpoint": true + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "ApiFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ApiFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "def handler(event, context):\n return {'body': 'Hello World!', 'statusCode': 200}" + }, + "Role": { + "Fn::GetAtt": [ + "ApiFunctionRole", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + }, + "ApiFunctionApiEventPermissionprod": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ApiGatewayApi" + } + } + ] + } + } + }, + "ApiGatewayApiprodStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ApiGatewayApiDeployment88816bd125" + }, + "RestApiId": { + "Ref": "ApiGatewayApi" + }, + "StageName": "prod" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 6ffee900b..914e4a55f 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -329,6 +329,7 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): "api_with_stage_tags", "api_with_mode", "api_with_no_properties", + "api_with_disable_api_execute_endpoint", "s3", "s3_create_remove", "s3_existing_lambda_notification_configuration", From a5db070f446b7cfebdaa6ad2e3dcf78f6105a272 Mon Sep 17 00:00:00 2001 From: Ruperto Torres <86501267+torresxb1@users.noreply.github.com> Date: Mon, 3 Jan 2022 14:41:36 -0800 Subject: [PATCH 21/59] fix: Py27hash fix (#2182) * Add third party py27hash code * Add Py27UniStr and unit tests * Add py27hash_fix utils and tests * Add to_py27_compatible_template and tests * Apply py27hash fix to wherever it is needed * Apply py27hash fix, all tests pass except api_with_any_method_in_swagger * apply py27hash fix in openapi + run black * remove py27 testing * remove other py27 references * black fixes * fixes/typos * remove py27 from tox.ini * refactoring * third party notice * black * Fix py27hash fix to deal with null events * Fix Py27UniStr repr for unicode literals * black reformat * Update _template_has_api_resource to check data type more defensively * Apply py27Dict in _get_authorizers * Apply Py27Dict to authorizers and gateway responses which will go into swagger * Update to_py27_compatible_template to handle parameter_values; Add Py27LongInt class * Rename _convert_to_py27_dict to _convert_to_py27_type * Apply Py27UniStr to path param name * Handle HttpApi resource under to_py27_compatible_template * Fix InvalidDocumentException to not sort different exceptions * black reformat * Remove unnecessary test files Co-authored-by: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> --- DEVELOPMENT_GUIDE.md | 11 +- MANIFEST.in | 1 + Makefile | 3 - THIRD_PARTY_LICENSES | 23 + appveyor-integration-test.yml | 2 - appveyor.yml | 2 - pyproject.toml | 2 +- samtranslator/intrinsics/actions.py | 15 +- samtranslator/model/api/api_generator.py | 31 +- samtranslator/model/apigateway.py | 108 ++- samtranslator/model/eventsources/push.py | 33 +- samtranslator/model/exceptions.py | 2 +- samtranslator/open_api/open_api.py | 65 +- samtranslator/parser/parser.py | 27 +- .../plugins/api/implicit_api_plugin.py | 5 +- samtranslator/swagger/swagger.py | 256 ++++-- samtranslator/third_party/__init__.py | 0 .../third_party/py27hash/__init__.py | 0 samtranslator/third_party/py27hash/hash.py | 169 ++++ samtranslator/translator/transform.py | 6 +- samtranslator/utils/py27hash_fix.py | 632 +++++++++++++ setup.cfg | 4 + setup.py | 6 +- .../input/function_with_null_events.yaml | 8 + .../input/state_machine_with_null_events.yaml | 7 + .../aws-cn/function_with_null_events.json | 57 ++ .../state_machine_with_null_events.json | 20 + .../aws-us-gov/function_with_null_events.json | 57 ++ .../state_machine_with_null_events.json | 20 + .../output/function_with_null_events.json | 57 ++ .../state_machine_with_null_events.json | 20 + tests/translator/test_translator.py | 38 +- tests/utils/test_py27hash_fix.py | 838 ++++++++++++++++++ tox.ini | 18 +- 34 files changed, 2287 insertions(+), 256 deletions(-) create mode 100644 THIRD_PARTY_LICENSES create mode 100644 samtranslator/third_party/__init__.py create mode 100644 samtranslator/third_party/py27hash/__init__.py create mode 100644 samtranslator/third_party/py27hash/hash.py create mode 100644 samtranslator/utils/py27hash_fix.py create mode 100644 tests/translator/input/function_with_null_events.yaml create mode 100644 tests/translator/input/state_machine_with_null_events.yaml create mode 100644 tests/translator/output/aws-cn/function_with_null_events.json create mode 100644 tests/translator/output/aws-cn/state_machine_with_null_events.json create mode 100644 tests/translator/output/aws-us-gov/function_with_null_events.json create mode 100644 tests/translator/output/aws-us-gov/state_machine_with_null_events.json create mode 100644 tests/translator/output/function_with_null_events.json create mode 100644 tests/translator/output/state_machine_with_null_events.json create mode 100644 tests/utils/test_py27hash_fix.py diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md index de44b115c..7c6a4356c 100644 --- a/DEVELOPMENT_GUIDE.md +++ b/DEVELOPMENT_GUIDE.md @@ -26,9 +26,8 @@ Environment Setup ----------------- ### 1. Install Python Versions -Our officially supported Python versions are 2.7, 3.6, 3.7 and 3.8. Follow the idioms from this [excellent cheatsheet](http://python-future.org/compatible_idioms.html) to -make sure your code is compatible with both Python 2.7 and 3 (>=3.6) versions. -Our CI/CD pipeline is setup to run unit tests against both Python 2.7 and 3 versions. So make sure you test it with both versions before sending a Pull Request. +Our officially supported Python versions are 3.6, 3.7 and 3.8. +Our CI/CD pipeline is setup to run unit tests against Python 3 versions. Make sure you test it before sending a Pull Request. See [Unit testing with multiple Python versions](#unit-testing-with-multiple-python-versions). [pyenv](https://github.com/pyenv/pyenv) is a great tool to @@ -41,12 +40,11 @@ easily setup multiple Python versions. For 1. Install PyEnv - `curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash` 1. Restart shell so the path changes take effect - `exec $SHELL` -1. `pyenv install 2.7.17` 1. `pyenv install 3.6.12` 1. `pyenv install 3.7.9` 1. `pyenv install 3.8.6` 1. Make Python versions available in the project: - `pyenv local 2.7.17 3.6.12 3.7.9 3.8.6` + `pyenv local 3.6.12 3.7.9 3.8.6` Note: also make sure the following lines were written into your `.bashrc` (or `.zshrc`, depending on which shell you are using): ``` @@ -117,11 +115,10 @@ Running Tests ### Unit testing with one Python version If you're trying to do a quick run, it's ok to use the current python version. Run `make pr`. -If you're using Python2.7, you can run `make pr2.7` instead. ### Unit testing with multiple Python versions -Currently, our officially supported Python versions are 2.7, 3.6, 3.7 and 3.8. For the most +Currently, our officially supported Python versions are 3.6, 3.7 and 3.8. For the most part, code that works in Python3.6 will work in Python3.7 and Python3.8. You only run into problems if you are trying to use features released in a higher version (for example features introduced into Python3.7 will not work in Python3.6). If you want to test in many versions, you can create a virtualenv for diff --git a/MANIFEST.in b/MANIFEST.in index bb322f072..b4639e49f 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,5 +5,6 @@ recursive-include samtranslator/validator/sam_schema *.json include samtranslator/policy_templates_data/policy_templates.json include samtranslator/policy_templates_data/schema.json include README.md +include THIRD_PARTY_LICENSES prune tests diff --git a/Makefile b/Makefile index b2b7fdd36..21f96495f 100755 --- a/Makefile +++ b/Makefile @@ -26,9 +26,6 @@ dev: test # Verifications to run before sending a pull request pr: black-check init dev -# Verifications to run before sending a pull request, skipping black check because black requires Python 3.6+ -pr2.7: init dev - define HELP_MESSAGE Usage: $ make [TARGETS] diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES new file mode 100644 index 000000000..44da5bf4f --- /dev/null +++ b/THIRD_PARTY_LICENSES @@ -0,0 +1,23 @@ +The AWS Serverless Application Model includes the following third-party software/licensing: + +** py27hash; version 1.0.2 -- https://pypi.org/project/py27hash/ +Copyright (c) 2020 NeuML LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +---------------- diff --git a/appveyor-integration-test.yml b/appveyor-integration-test.yml index 0dc1cdf12..68c4ee7a9 100644 --- a/appveyor-integration-test.yml +++ b/appveyor-integration-test.yml @@ -3,8 +3,6 @@ image: Ubuntu environment: matrix: - - TOXENV: py27 - PYTHON_VERSION: '2.7' - TOXENV: py36 PYTHON_VERSION: '3.6' - TOXENV: py37 diff --git a/appveyor.yml b/appveyor.yml index d20b784b9..1552fc748 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,8 +3,6 @@ image: Ubuntu environment: matrix: - - TOXENV: py27 - PYTHON_VERSION: '2.7' - TOXENV: py36 PYTHON_VERSION: '3.6' - TOXENV: py37 diff --git a/pyproject.toml b/pyproject.toml index 35465959e..628fb93e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 120 -target_version = ['py27', 'py37', 'py36', 'py38'] +target_version = ['py37', 'py36', 'py38'] exclude = ''' ( diff --git a/samtranslator/intrinsics/actions.py b/samtranslator/intrinsics/actions.py index 9711dedfb..7eafbde63 100644 --- a/samtranslator/intrinsics/actions.py +++ b/samtranslator/intrinsics/actions.py @@ -1,6 +1,7 @@ import re from six import string_types +from samtranslator.utils.py27hash_fix import Py27UniStr from samtranslator.model.exceptions import InvalidTemplateException, InvalidDocumentException @@ -375,13 +376,13 @@ def handler_method(full_ref, ref_value): # Find all the pattern, and call the handler to decide how to substitute them. # Do the substitution and return the final text - return re.sub( - ref_pattern, - # Pass the handler entire string ${logicalId.property} as first parameter and "logicalId.property" - # as second parameter. Return value will be substituted - lambda match: handler_method(match.group(0), match.group(1)), - text, - ) + # NOTE: in order to make sure Py27UniStr strings won't be converted to plain string, + # we need to iterate through each match and do the replacement + substituted = text + for match in re.finditer(ref_pattern, text): + sub_value = handler_method(match.group(0), match.group(1)) + substituted = substituted.replace(match.group(0), sub_value, 1) + return substituted class GetAttAction(Action): diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 0fab23e58..c1a07fcdf 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -27,6 +27,7 @@ from samtranslator.translator import logical_id_generator from samtranslator.translator.arn_generator import ArnGenerator from samtranslator.model.tags.resource_tagging import get_tag_list +from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr LOG = logging.getLogger(__name__) @@ -338,7 +339,18 @@ def _construct_body_s3_dict(self): "'s3://bucket/key' with optional versionId query parameter.", ) - body_s3 = {"Bucket": s3_pointer["Bucket"], "Key": s3_pointer["Key"]} + if isinstance(self.definition_uri, Py27UniStr): + # self.defintion_uri is a Py27UniStr instance if it is defined in the template + # we need to preserve the Py27UniStr type + s3_pointer["Bucket"] = Py27UniStr(s3_pointer["Bucket"]) + s3_pointer["Key"] = Py27UniStr(s3_pointer["Key"]) + if "Version" in s3_pointer: + s3_pointer["Version"] = Py27UniStr(s3_pointer["Version"]) + + # Construct body_s3 as py27 dict + body_s3 = Py27Dict() + body_s3["Bucket"] = s3_pointer["Bucket"] + body_s3["Key"] = s3_pointer["Key"] if "Version" in s3_pointer: body_s3["Version"] = s3_pointer["Version"] return body_s3 @@ -922,12 +934,13 @@ def _add_gateway_responses(self): swagger_editor = SwaggerEditor(self.definition_body) - gateway_responses = {} + # The dicts below will eventually become part of swagger/openapi definition, thus requires using Py27Dict() + gateway_responses = Py27Dict() for response_type, response in self.gateway_responses.items(): gateway_responses[response_type] = ApiGatewayResponse( api_logical_id=self.logical_id, - response_parameters=response.get("ResponseParameters", {}), - response_templates=response.get("ResponseTemplates", {}), + response_parameters=response.get("ResponseParameters", Py27Dict()), + response_templates=response.get("ResponseTemplates", Py27Dict()), status_code=response.get("StatusCode", None), ) @@ -985,12 +998,12 @@ def _openapi_postprocess(self, definition_body): SwaggerEditor.get_openapi_version_3_regex(), self.open_api_version ): if definition_body.get("securityDefinitions"): - components = definition_body.get("components", {}) + components = definition_body.get("components", Py27Dict()) components["securitySchemes"] = definition_body["securityDefinitions"] definition_body["components"] = components del definition_body["securityDefinitions"] if definition_body.get("definitions"): - components = definition_body.get("components", {}) + components = definition_body.get("components", Py27Dict()) components["schemas"] = definition_body["definitions"] definition_body["components"] = components del definition_body["definitions"] @@ -1016,7 +1029,8 @@ def _openapi_postprocess(self, definition_body): if field_val.get("200") and field_val.get("200").get("headers"): headers = field_val["200"]["headers"] for header, header_val in headers.items(): - new_header_val_with_schema = {"schema": header_val} + new_header_val_with_schema = Py27Dict() + new_header_val_with_schema["schema"] = header_val definition_body["paths"][path]["options"][field]["200"]["headers"][ header ] = new_header_val_with_schema @@ -1024,7 +1038,8 @@ def _openapi_postprocess(self, definition_body): return definition_body def _get_authorizers(self, authorizers_config, default_authorizer=None): - authorizers = {} + # The dict below will eventually become part of swagger/openapi definition, thus requires using Py27Dict() + authorizers = Py27Dict() if default_authorizer == "AWS_IAM": authorizers[default_authorizer] = ApiGatewayAuthorizer( api_logical_id=self.logical_id, name=default_authorizer, is_aws_iam_authorizer=True diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 5883fd716..41dd19f76 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -1,11 +1,13 @@ import json from re import match +from functools import reduce from samtranslator.model import PropertyType, Resource from samtranslator.model.exceptions import InvalidResourceException from samtranslator.model.types import is_type, one_of, is_str, list_of from samtranslator.model.intrinsics import ref, fnSub from samtranslator.translator import logical_id_generator from samtranslator.translator.arn_generator import ArnGenerator +from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr class ApiGatewayRestApi(Resource): @@ -128,15 +130,16 @@ def __init__(self, api_logical_id=None, response_parameters=None, response_templ raise InvalidResourceException(api_logical_id, "Property 'StatusCode' must be numeric") self.api_logical_id = api_logical_id - self.response_parameters = response_parameters or {} - self.response_templates = response_templates or {} + # Defaults to Py27Dict() as these will go into swagger + self.response_parameters = response_parameters or Py27Dict() + self.response_templates = response_templates or Py27Dict() self.status_code = status_code_str def generate_swagger(self): - swagger = { - "responseParameters": self._add_prefixes(self.response_parameters), - "responseTemplates": self.response_templates, - } + # Applying Py27Dict here as this goes into swagger + swagger = Py27Dict() + swagger["responseParameters"] = self._add_prefixes(self.response_parameters) + swagger["responseTemplates"] = self.response_templates # Prevent "null" being written. if self.status_code: @@ -146,13 +149,17 @@ def generate_swagger(self): def _add_prefixes(self, response_parameters): GATEWAY_RESPONSE_PREFIX = "gatewayresponse." - prefixed_parameters = {} - for key, value in response_parameters.get("Headers", {}).items(): - prefixed_parameters[GATEWAY_RESPONSE_PREFIX + "header." + key] = value - for key, value in response_parameters.get("Paths", {}).items(): - prefixed_parameters[GATEWAY_RESPONSE_PREFIX + "path." + key] = value - for key, value in response_parameters.get("QueryStrings", {}).items(): - prefixed_parameters[GATEWAY_RESPONSE_PREFIX + "querystring." + key] = value + # applying Py27Dict as this is part of swagger + prefixed_parameters = Py27Dict() + + parameter_prefix_pairs = [("Headers", "header."), ("Paths", "path."), ("QueryStrings", "querystring.")] + for parameter, prefix in parameter_prefix_pairs: + for key, value in response_parameters.get(parameter, {}).items(): + param_key = GATEWAY_RESPONSE_PREFIX + prefix + key + if isinstance(key, Py27UniStr): + # if key is from template, we need to convert param_key to Py27UniStr + param_key = Py27UniStr(param_key) + prefixed_parameters[param_key] = value return prefixed_parameters @@ -288,21 +295,20 @@ def _is_missing_identity_source(self, identity): def generate_swagger(self): authorizer_type = self._get_type() APIGATEWAY_AUTHORIZER_KEY = "x-amazon-apigateway-authorizer" - swagger = { - "type": "apiKey", - "name": self._get_swagger_header_name(), - "in": "header", - "x-amazon-apigateway-authtype": self._get_swagger_authtype(), - } + swagger = Py27Dict() + swagger["type"] = "apiKey" + swagger["name"] = self._get_swagger_header_name() + swagger["in"] = "header" + swagger["x-amazon-apigateway-authtype"] = self._get_swagger_authtype() if authorizer_type == "COGNITO_USER_POOLS": - swagger[APIGATEWAY_AUTHORIZER_KEY] = { - "type": self._get_swagger_authorizer_type(), - "providerARNs": self._get_user_pool_arn_array(), - } + authorizer_dict = Py27Dict() + authorizer_dict["type"] = self._get_swagger_authorizer_type() + authorizer_dict["providerARNs"] = self._get_user_pool_arn_array() + swagger[APIGATEWAY_AUTHORIZER_KEY] = authorizer_dict elif authorizer_type == "LAMBDA": - swagger[APIGATEWAY_AUTHORIZER_KEY] = {"type": self._get_swagger_authorizer_type()} + swagger[APIGATEWAY_AUTHORIZER_KEY] = Py27Dict({"type": self._get_swagger_authorizer_type()}) partition = ArnGenerator.get_partition_name() resource = "lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations" authorizer_uri = fnSub( @@ -341,35 +347,39 @@ def generate_swagger(self): def _get_identity_validation_expression(self): return self.identity and self.identity.get("ValidationExpression") - def _get_identity_source(self): - identity_source_headers = [] - identity_source_query_strings = [] - identity_source_stage_variables = [] - identity_source_context = [] - - if self.identity.get("Headers"): - identity_source_headers = list(map(lambda h: "method.request.header." + h, self.identity.get("Headers"))) - - if self.identity.get("QueryStrings"): - identity_source_query_strings = list( - map(lambda qs: "method.request.querystring." + qs, self.identity.get("QueryStrings")) - ) + def _build_identity_source_item(self, item_prefix, prop_value): + item = item_prefix + prop_value + if isinstance(prop_value, Py27UniStr): + item = Py27UniStr(item) + return item - if self.identity.get("StageVariables"): - identity_source_stage_variables = list( - map(lambda sv: "stageVariables." + sv, self.identity.get("StageVariables")) - ) - - if self.identity.get("Context"): - identity_source_context = list(map(lambda c: "context." + c, self.identity.get("Context"))) + def _build_identity_source_item_array(self, prop_key, item_prefix): + arr = [] + if self.identity.get(prop_key): + arr = [ + self._build_identity_source_item(item_prefix, prop_value) for prop_value in self.identity.get(prop_key) + ] + return arr - identity_source_array = ( - identity_source_headers - + identity_source_query_strings - + identity_source_stage_variables - + identity_source_context + def _get_identity_source(self): + key_prefix_pairs = [ + ("Headers", "method.request.header."), + ("QueryStrings", "method.request.querystring."), + ("StageVariables", "stageVariables."), + ("Context", "context."), + ] + + identity_source_array = reduce( + lambda accumulator, key_prefix_pair: accumulator + + self._build_identity_source_item_array(key_prefix_pair[0], key_prefix_pair[1]), + key_prefix_pairs, + [], ) + identity_source = ", ".join(identity_source_array) + if any(isinstance(i, Py27UniStr) for i in identity_source_array): + # Convert identity_source to Py27UniStr if any part of it is Py27UniStr + identity_source = Py27UniStr(identity_source) return identity_source diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index ff444a6cc..eb3bf5798 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -23,6 +23,7 @@ from samtranslator.model.exceptions import InvalidEventException, InvalidResourceException from samtranslator.swagger.swagger import SwaggerEditor from samtranslator.open_api.open_api import OpenApiEditor +from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr CONDITION = "Condition" @@ -663,15 +664,8 @@ def _add_swagger_integration(self, api, function, intrinsics_resolver): if swagger_body is None: return - function_arn = function.get_runtime_attr("arn") partition = ArnGenerator.get_partition_name() - uri = fnSub( - "arn:" - + partition - + ":apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/" - + make_shorthand(function_arn) - + "/invocations" - ) + uri = _build_apigw_integration_uri(function, partition) editor = SwaggerEditor(swagger_body) @@ -1162,12 +1156,7 @@ def _add_openapi_integration(self, api, function, manage_swagger=False): if open_api_body is None: return - function_arn = function.get_runtime_attr("arn") - uri = fnSub( - "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/" - + make_shorthand(function_arn) - + "/invocations" - ) + uri = _build_apigw_integration_uri(function, "${AWS::Partition}") editor = OpenApiEditor(open_api_body) @@ -1263,3 +1252,19 @@ def _add_auth_to_openapi_integration(self, api, editor): "'AuthorizationScopes' must be a list of strings.".format(method=self.Method, path=self.Path), ) editor.add_auth_to_method(api=api, path=self.Path, method_name=self.Method, auth=self.Auth) + + +def _build_apigw_integration_uri(function, partition): + function_arn = function.get_runtime_attr("arn") + arn = ( + "arn:" + + partition + + ":apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/" + + make_shorthand(function_arn) + + "/invocations" + ) + # function_arn is always of the form {"Fn::GetAtt": ["", "Arn"]}. + # We only want to check if the function logical id is a Py27UniStr instance. + if function_arn.get("Fn::GetAtt") and isinstance(function_arn["Fn::GetAtt"][0], Py27UniStr): + arn = Py27UniStr(arn) + return Py27Dict(fnSub(arn)) diff --git a/samtranslator/model/exceptions.py b/samtranslator/model/exceptions.py index 153954b61..cd4a6fdad 100644 --- a/samtranslator/model/exceptions.py +++ b/samtranslator/model/exceptions.py @@ -7,7 +7,7 @@ class InvalidDocumentException(Exception): """ def __init__(self, causes): - self._causes = sorted(causes) + self._causes = causes @property def message(self): diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index 9639a5baf..298ec7ead 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -6,6 +6,7 @@ from samtranslator.model.intrinsics import make_conditional from samtranslator.model.intrinsics import is_intrinsic from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException +from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr import json @@ -15,6 +16,12 @@ class OpenApiEditor(object): cares about. It is built to handle "partial Swagger" ie. Swagger that is incomplete and won't pass the Swagger spec. But this is necessary for SAM because it iteratively builds the Swagger starting from an empty skeleton. + + NOTE (hawflau): To ensure the same logical ID will be generated in Py3 as in Py2 for AWS::Serverless::HttpApi resource, + we have to apply py27hash_fix. For any dictionary that is created within the swagger body, we need to initiate it + with Py27Dict() instead of {}. We also need to add keys into the Py27Dict instance one by one, so that the input + order could be preserved. This is a must for the purpose of preserving the dict key iteration order, which is + essential for generating the same logical ID. """ _X_APIGW_INTEGRATION = "x-amazon-apigateway-integration" @@ -43,10 +50,10 @@ def __init__(self, doc): self._doc = copy.deepcopy(doc) self.paths = self._doc["paths"] - self.security_schemes = self._doc.get("components", {}).get("securitySchemes", {}) - self.definitions = self._doc.get("definitions", {}) + self.security_schemes = self._doc.get("components", Py27Dict()).get("securitySchemes", Py27Dict()) + self.definitions = self._doc.get("definitions", Py27Dict()) self.tags = self._doc.get("tags", []) - self.info = self._doc.get("info", {}) + self.info = self._doc.get("info", Py27Dict()) def get_path(self, path): """ @@ -89,7 +96,7 @@ def get_integration_function_logical_id(self, path_name, method_name): # Get the method contents # We only want the first one in case there are multiple (in a conditional) method = self.get_method_contents(path[method_name])[0] - integration = method.get(self._X_APIGW_INTEGRATION, {}) + integration = method.get(self._X_APIGW_INTEGRATION, Py27Dict()) # Extract the integration uri out of a conditional if necessary uri = integration.get("uri") @@ -176,7 +183,7 @@ def add_path(self, path, method=None): """ method = self._normalize_method_name(method) - path_dict = self.paths.setdefault(path, {}) + path_dict = self.paths.setdefault(path, Py27Dict()) if not isinstance(path_dict, dict): # Either customers has provided us an invalid Swagger, or this class has messed it somehow @@ -191,7 +198,7 @@ def add_path(self, path, method=None): if self._CONDITIONAL_IF in path_dict: path_dict = path_dict[self._CONDITIONAL_IF][1] - path_dict.setdefault(method, {}) + path_dict.setdefault(method, Py27Dict()) def add_lambda_integration( self, path, method, integration_uri, method_auth_config=None, api_auth_config=None, condition=None @@ -217,18 +224,18 @@ def add_lambda_integration( integration_uri = make_conditional(condition, integration_uri) path_dict = self.get_path(path) - path_dict[method][self._X_APIGW_INTEGRATION] = { - "type": "aws_proxy", - "httpMethod": "POST", - "payloadFormatVersion": "2.0", - "uri": integration_uri, - } + # create as Py27Dict and insert key one by one to preserve input order + path_dict[method][self._X_APIGW_INTEGRATION] = Py27Dict() + path_dict[method][self._X_APIGW_INTEGRATION]["type"] = "aws_proxy" + path_dict[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" + path_dict[method][self._X_APIGW_INTEGRATION]["payloadFormatVersion"] = "2.0" + path_dict[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri if path == self._DEFAULT_PATH and method == self._X_ANY_METHOD: path_dict[method]["isDefaultRoute"] = True # If 'responses' key is *not* present, add it with an empty dict as value - path_dict[method].setdefault("responses", {}) + path_dict[method].setdefault("responses", Py27Dict()) # If a condition is present, wrap all method contents up into the condition if condition: @@ -296,7 +303,12 @@ def add_path_parameters_to_method(self, api, path, method_name, path_parameters) existing_parameter["in"] = "path" existing_parameter["required"] = True else: - parameter = {"name": param, "in": "path", "required": True} + # create as Py27Dict and insert keys one by one to preserve input order + parameter = Py27Dict() + param = Py27UniStr(param) if isinstance(param, str) else param + parameter["name"] = param + parameter["in"] = "path" + parameter["required"] = True method_definition.get("parameters").append(parameter) def add_payload_format_version_to_method(self, api, path, method_name, payload_format_version="2.0"): @@ -319,7 +331,7 @@ def add_authorizers_security_definitions(self, authorizers): :param list authorizers: List of Authorizer configurations which get translated to securityDefinitions. """ - self.security_schemes = self.security_schemes or {} + self.security_schemes = self.security_schemes or Py27Dict() for authorizer_name, authorizer in authorizers.items(): self.security_schemes[authorizer_name] = authorizer.generate_openapi() @@ -444,7 +456,10 @@ def add_tags(self, tags): # overwrite tag value for an existing tag existing_tag[self._X_APIGW_TAG_VALUE] = value else: - tag = {"name": name, self._X_APIGW_TAG_VALUE: value} + # create as Py27Dict and insert key one by one to preserve input order + tag = Py27Dict() + tag["name"] = name + tag[self._X_APIGW_TAG_VALUE] = value self.tags.append(tag) def add_endpoint_config(self, disable_execute_api_endpoint): @@ -460,7 +475,7 @@ def add_endpoint_config(self, disable_execute_api_endpoint): DISABLE_EXECUTE_API_ENDPOINT = "disableExecuteApiEndpoint" - servers_configurations = self._doc.get(self._SERVERS, [{}]) + servers_configurations = self._doc.get(self._SERVERS, [Py27Dict()]) for config in servers_configurations: endpoint_configuration = config.get(self._X_APIGW_ENDPOINT_CONFIG, dict()) endpoint_configuration[DISABLE_EXECUTE_API_ENDPOINT] = disable_execute_api_endpoint @@ -560,7 +575,7 @@ def openapi(self): self._doc["tags"] = self.tags if self.security_schemes: - self._doc.setdefault("components", {}) + self._doc.setdefault("components", Py27Dict()) self._doc["components"]["securitySchemes"] = self.security_schemes if self.info: @@ -591,7 +606,14 @@ def gen_skeleton(): :return dict: Dictionary of a skeleton swagger document """ - return {"openapi": "3.0.1", "info": {"version": "1.0", "title": ref("AWS::StackName")}, "paths": {}} + # create as Py27Dict and insert key one by one to preserve input order + skeleton = Py27Dict() + skeleton["openapi"] = "3.0.1" + skeleton["info"] = Py27Dict() + skeleton["info"]["version"] = "1.0" + skeleton["info"]["title"] = ref("AWS::StackName") + skeleton["paths"] = Py27Dict() + return skeleton @staticmethod def _get_authorization_scopes(authorizers, default_authorizer): @@ -639,4 +661,7 @@ def safe_compare_regex_with_string(regex, data): @staticmethod def get_path_without_trailing_slash(path): - return re.sub(r"{([a-zA-Z0-9._-]+|proxy\+)}", "*", path) + sub = re.sub(r"{([a-zA-Z0-9._-]+|proxy\+)}", "*", path) + if isinstance(path, Py27UniStr): + return Py27UniStr(sub) + return sub diff --git a/samtranslator/parser/parser.py b/samtranslator/parser/parser.py index b060cc028..9d07e43c2 100644 --- a/samtranslator/parser/parser.py +++ b/samtranslator/parser/parser.py @@ -16,16 +16,9 @@ def parse(self, sam_template, parameter_values, sam_plugins): self._validate(sam_template, parameter_values) sam_plugins.act(LifeCycleEvents.before_transform_template, sam_template) - # private methods - def _validate(self, sam_template, parameter_values): - """Validates the template and parameter values and raises exceptions if there's an issue - - :param dict sam_template: SAM template - :param dict parameter_values: Dictionary of parameter values provided by the user - """ - if parameter_values is None: - raise ValueError("`parameter_values` argument is required") - + @staticmethod + def validate_datatypes(sam_template): + """Validates the datatype within the template """ if ( "Resources" not in sam_template or not isinstance(sam_template["Resources"], dict) @@ -58,10 +51,22 @@ def _validate(self, sam_template, parameter_values): ) ] ) + + # private methods + def _validate(self, sam_template, parameter_values): + """Validates the template and parameter values and raises exceptions if there's an issue + + :param dict sam_template: SAM template + :param dict parameter_values: Dictionary of parameter values provided by the user + """ + if parameter_values is None: + raise ValueError("`parameter_values` argument is required") + + Parser.validate_datatypes(sam_template) + try: validator = SamTemplateValidator() validation_errors = validator.validate(sam_template) - if validation_errors: LOG.warning("Template schema validation reported the following errors: %s", validation_errors) except Exception as e: diff --git a/samtranslator/plugins/api/implicit_api_plugin.py b/samtranslator/plugins/api/implicit_api_plugin.py index 470176210..99397a9c6 100644 --- a/samtranslator/plugins/api/implicit_api_plugin.py +++ b/samtranslator/plugins/api/implicit_api_plugin.py @@ -7,6 +7,7 @@ from samtranslator.public.exceptions import InvalidDocumentException, InvalidResourceException, InvalidEventException from samtranslator.public.sdk.resource import SamResourceType from samtranslator.public.sdk.template import SamTemplate +from samtranslator.utils.py27hash_fix import Py27Dict class ImplicitApiPlugin(BasePlugin): @@ -122,9 +123,9 @@ def _get_api_events(self, resource): and isinstance(resource.properties.get("Events"), dict) ): # Resource structure is invalid. - return {} + return Py27Dict() - api_events = {} + api_events = Py27Dict() for event_id, event in resource.properties["Events"].items(): if event and isinstance(event, dict) and event.get("Type") == self.api_event_type: diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 3151095ed..1466e7329 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -6,6 +6,7 @@ from samtranslator.model.intrinsics import ref from samtranslator.model.intrinsics import make_conditional, fnSub from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException +from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr class SwaggerEditor(object): @@ -14,6 +15,12 @@ class SwaggerEditor(object): cares about. It is built to handle "partial Swagger" ie. Swagger that is incomplete and won't pass the Swagger spec. But this is necessary for SAM because it iteratively builds the Swagger starting from an empty skeleton. + + NOTE (hawflau): To ensure the same logical ID will be generate in Py3 as in Py2 for AWS::Serverless::Api resource, + we have to apply py27hash_fix. For any dictionary that is created within the swagger body, we need to initiate it + with Py27Dict() instead of {}. We also need to add keys into the Py27Dict instance one by one, so that the input + order could be preserved. This is a must for the purpose of preserving the dict key iteration order, which is + essential for generating the same logical ID. """ _OPTIONS_METHOD = "options" @@ -48,10 +55,10 @@ def __init__(self, doc): self._doc = copy.deepcopy(doc) self.paths = self._doc["paths"] - self.security_definitions = self._doc.get("securityDefinitions", {}) - self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, {}) - self.resource_policy = self._doc.get(self._X_APIGW_POLICY, {}) - self.definitions = self._doc.get("definitions", {}) + self.security_definitions = self._doc.get("securityDefinitions", Py27Dict()) + self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, Py27Dict()) + self.resource_policy = self._doc.get(self._X_APIGW_POLICY, Py27Dict()) + self.definitions = self._doc.get("definitions", Py27Dict()) # https://swagger.io/specification/#path-item-object # According to swagger spec, @@ -160,13 +167,13 @@ def add_path(self, path, method=None): """ method = self._normalize_method_name(method) - path_dict = self.paths.setdefault(path, {}) + path_dict = self.paths.setdefault(path, Py27Dict()) SwaggerEditor.validate_path_item_is_dict(path_dict, path) if self._CONDITIONAL_IF in path_dict: path_dict = path_dict[self._CONDITIONAL_IF][1] - path_dict.setdefault(method, {}) + path_dict.setdefault(method, Py27Dict()) def add_lambda_integration( self, path, method, integration_uri, method_auth_config=None, api_auth_config=None, condition=None @@ -191,14 +198,14 @@ def add_lambda_integration( integration_uri = make_conditional(condition, integration_uri) path_dict = self.get_path(path) - path_dict[method][self._X_APIGW_INTEGRATION] = { - "type": "aws_proxy", - "httpMethod": "POST", - "uri": integration_uri, - } - - method_auth_config = method_auth_config or {} - api_auth_config = api_auth_config or {} + path_dict[method][self._X_APIGW_INTEGRATION] = Py27Dict() + # insert key one by one to preserce input order + path_dict[method][self._X_APIGW_INTEGRATION]["type"] = "aws_proxy" + path_dict[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" + path_dict[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri + + method_auth_config = method_auth_config or Py27Dict() + api_auth_config = api_auth_config or Py27Dict() if ( method_auth_config.get("Authorizer") == "AWS_IAM" or api_auth_config.get("DefaultAuthorizer") == "AWS_IAM" @@ -217,7 +224,7 @@ def add_lambda_integration( self.paths[path][method][self._X_APIGW_INTEGRATION]["credentials"] = credentials # If 'responses' key is *not* present, add it with an empty dict as value - path_dict[method].setdefault("responses", {}) + path_dict[method].setdefault("responses", Py27Dict()) # If a condition is present, wrap all method contents up into the condition if condition: @@ -257,16 +264,23 @@ def add_state_machine_integration( path_dict = self.get_path(path) # Responses - integration_responses = {"200": {"statusCode": "200"}, "400": {"statusCode": "400"}} - default_method_responses = {"200": {"description": "OK"}, "400": {"description": "Bad Request"}} - - path_dict[method][self._X_APIGW_INTEGRATION] = { - "type": "aws", - "httpMethod": "POST", - "uri": integration_uri, - "responses": integration_responses, - "credentials": credentials, - } + integration_responses = Py27Dict() + # insert key one by one to preserce input order + integration_responses["200"] = Py27Dict({"statusCode": "200"}) + integration_responses["400"] = Py27Dict({"statusCode": "400"}) + + default_method_responses = Py27Dict() + # insert key one by one to preserce input order + default_method_responses["200"] = Py27Dict({"description": "OK"}) + default_method_responses["400"] = Py27Dict({"description": "Bad Request"}) + + path_dict[method][self._X_APIGW_INTEGRATION] = Py27Dict() + # insert key one by one to preserce input order + path_dict[method][self._X_APIGW_INTEGRATION]["type"] = "aws" + path_dict[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" + path_dict[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri + path_dict[method][self._X_APIGW_INTEGRATION]["responses"] = integration_responses + path_dict[method][self._X_APIGW_INTEGRATION]["credentials"] = credentials # If 'responses' key is *not* present, add it with an empty dict as value path_dict[method].setdefault("responses", default_method_responses) @@ -353,7 +367,25 @@ def add_cors( ) def add_binary_media_types(self, binary_media_types): - bmt = json.loads(json.dumps(binary_media_types).replace("~1", "/")) + """ + Args: + binary_media_types: list + """ + + def replace_recursively(bmt): + """replaces "~1" with "/" for the input binary_media_types recursively""" + if isinstance(bmt, dict): + to_return = Py27Dict() + for k, v in bmt.items(): + to_return[Py27UniStr(k.replace("~1", "/"))] = replace_recursively(v) + return to_return + if isinstance(bmt, list): + return [replace_recursively(item) for item in bmt] + if isinstance(bmt, string_types) or isinstance(bmt, Py27UniStr): + return Py27UniStr(bmt.replace("~1", "/")) + return bmt + + bmt = replace_recursively(binary_media_types) self._doc[self._X_APIGW_BINARY_MEDIA_TYPES] = bmt def _options_method_response_for_cors( @@ -385,15 +417,19 @@ def _options_method_response_for_cors( ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials" HEADER_RESPONSE = lambda x: "method.response.header." + x - response_parameters = { - # AllowedOrigin is always required - HEADER_RESPONSE(ALLOW_ORIGIN): allowed_origins - } + response_parameters = Py27Dict( + { + # AllowedOrigin is always required + HEADER_RESPONSE(ALLOW_ORIGIN): allowed_origins + } + ) - response_headers = { - # Allow Origin is always required - ALLOW_ORIGIN: {"type": "string"} - } + response_headers = Py27Dict( + { + # Allow Origin is always required + ALLOW_ORIGIN: {"type": "string"} + } + ) # Optional values. Skip the header if value is empty # @@ -417,23 +453,24 @@ def _options_method_response_for_cors( response_parameters[HEADER_RESPONSE(ALLOW_CREDENTIALS)] = "'true'" response_headers[ALLOW_CREDENTIALS] = {"type": "string"} - return { - "summary": "CORS support", - "consumes": ["application/json"], - "produces": ["application/json"], - self._X_APIGW_INTEGRATION: { - "type": "mock", - "requestTemplates": {"application/json": '{\n "statusCode" : 200\n}\n'}, - "responses": { - "default": { - "statusCode": "200", - "responseParameters": response_parameters, - "responseTemplates": {"application/json": "{}\n"}, - } - }, - }, - "responses": {"200": {"description": "Default response for CORS method", "headers": response_headers}}, - } + # construct snippet and insert key one by one to preserce input order + to_return = Py27Dict() + to_return["summary"] = "CORS support" + to_return["consumes"] = ["application/json"] + to_return["produces"] = ["application/json"] + to_return[self._X_APIGW_INTEGRATION] = Py27Dict() + to_return[self._X_APIGW_INTEGRATION]["type"] = "mock" + to_return[self._X_APIGW_INTEGRATION]["requestTemplates"] = {"application/json": '{\n "statusCode" : 200\n}\n'} + to_return[self._X_APIGW_INTEGRATION]["responses"] = Py27Dict() + to_return[self._X_APIGW_INTEGRATION]["responses"]["default"] = Py27Dict() + to_return[self._X_APIGW_INTEGRATION]["responses"]["default"]["statusCode"] = "200" + to_return[self._X_APIGW_INTEGRATION]["responses"]["default"]["responseParameters"] = response_parameters + to_return[self._X_APIGW_INTEGRATION]["responses"]["default"]["responseTemplates"] = {"application/json": "{}\n"} + to_return["responses"] = Py27Dict() + to_return["responses"]["200"] = Py27Dict() + to_return["responses"]["200"]["description"] = "Default response for CORS method" + to_return["responses"]["200"]["headers"] = response_headers + return to_return def _make_cors_allowed_methods_for_path(self, path): """ @@ -479,7 +516,7 @@ def add_authorizers_security_definitions(self, authorizers): :param list authorizers: List of Authorizer configurations which get translated to securityDefinitions. """ - self.security_definitions = self.security_definitions or {} + self.security_definitions = self.security_definitions or Py27Dict() for authorizer_name, authorizer in authorizers.items(): self.security_definitions[authorizer_name] = authorizer.generate_swagger() @@ -490,16 +527,15 @@ def add_awsiam_security_definition(self): Note: this method is idempotent """ - aws_iam_security_definition = { - "AWS_IAM": { - "x-amazon-apigateway-authtype": "awsSigv4", - "type": "apiKey", - "name": "Authorization", - "in": "header", - } - } + # construct aws_iam_security_definition as Py27Dict and insert key one by one to preserce input order + aws_iam_security_definition = Py27Dict() + aws_iam_security_definition["AWS_IAM"] = Py27Dict() + aws_iam_security_definition["AWS_IAM"]["x-amazon-apigateway-authtype"] = "awsSigv4" + aws_iam_security_definition["AWS_IAM"]["type"] = "apiKey" + aws_iam_security_definition["AWS_IAM"]["name"] = "Authorization" + aws_iam_security_definition["AWS_IAM"]["in"] = "header" - self.security_definitions = self.security_definitions or {} + self.security_definitions = self.security_definitions or Py27Dict() # Only add the security definition if it doesn't exist. This helps ensure # that we minimize changes to the swagger in the case of user defined swagger @@ -512,9 +548,15 @@ def add_apikey_security_definition(self): Note: this method is idempotent """ - api_key_security_definition = {"api_key": {"type": "apiKey", "name": "x-api-key", "in": "header"}} + # construct api_key_security_definiton as py27 dict + # and insert keys one by one to preserve input order + api_key_security_definition = Py27Dict() + api_key_security_definition["api_key"] = Py27Dict() + api_key_security_definition["api_key"]["type"] = "apiKey" + api_key_security_definition["api_key"]["name"] = "x-api-key" + api_key_security_definition["api_key"]["in"] = "header" - self.security_definitions = self.security_definitions or {} + self.security_definitions = self.security_definitions or Py27Dict() # Only add the security definition if it doesn't exist. This helps ensure # that we minimize changes to the swagger in the case of user defined swagger @@ -606,7 +648,7 @@ def set_path_default_authorizer( # No existing Authorizer found; use default else: - security_dict = {} + security_dict = Py27Dict() security_dict[default_authorizer] = self._get_authorization_scopes( api_authorizers, default_authorizer ) @@ -678,7 +720,7 @@ def set_path_default_apikey_required(self, path): # No existing ApiKey setting found or it's already set to the default else: - security_dict = {} + security_dict = Py27Dict() security_dict["api_key"] = [] apikey_security = [security_dict] @@ -720,7 +762,7 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers authorizers param. """ if authorizers is None: - authorizers = {} + authorizers = Py27Dict() normalized_method_name = self._normalize_method_name(method_name) # It is possible that the method could have two definitions in a Fn::If block. for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): @@ -731,7 +773,7 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers existing_security = method_definition.get("security", []) - security_dict = {} + security_dict = Py27Dict() security_dict[authorizer_name] = [] authorizer_security = [security_dict] @@ -739,7 +781,7 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers security = existing_security + authorizer_security if authorizer_name != "NONE" and authorizers: - method_auth_scopes = authorizers.get(authorizer_name, {}).get("AuthorizationScopes") + method_auth_scopes = authorizers.get(authorizer_name, Py27Dict()).get("AuthorizationScopes") if method_scopes is not None: method_auth_scopes = method_scopes if authorizers.get(authorizer_name) is not None and method_auth_scopes is not None: @@ -774,7 +816,7 @@ def _set_method_apikey_handling(self, path, method_name, apikey_required): if apikey_required: # We want to enable apikey required security - security_dict = {} + security_dict = Py27Dict() security_dict["api_key"] = [] apikey_security = [security_dict] self.add_apikey_security_definition() @@ -782,7 +824,7 @@ def _set_method_apikey_handling(self, path, method_name, apikey_required): # The method explicitly does NOT require apikey and there is an API default # so let's add a marker 'api_key_false' so that we don't incorrectly override # with the api default - security_dict = {} + security_dict = Py27Dict() security_dict["api_key_false"] = [] apikey_security = [security_dict] @@ -805,12 +847,15 @@ def add_request_validator_to_method(self, path, method_name, validate_body=False normalized_method_name = self._normalize_method_name(method_name) validator_name = SwaggerEditor.get_validator_name(validate_body, validate_parameters) - # Creating validator - request_validator_definition = { - validator_name: {"validateRequestBody": validate_body, "validateRequestParameters": validate_parameters} - } + # Creating validator as py27 dict + # and insert keys one by one to preserve input order + request_validator_definition = Py27Dict() + request_validator_definition[validator_name] = Py27Dict() + request_validator_definition[validator_name]["validateRequestBody"] = validate_body + request_validator_definition[validator_name]["validateRequestParameters"] = validate_parameters + if not self._doc.get(self._X_APIGW_REQUEST_VALIDATORS): - self._doc[self._X_APIGW_REQUEST_VALIDATORS] = {} + self._doc[self._X_APIGW_REQUEST_VALIDATORS] = Py27Dict() if not self._doc[self._X_APIGW_REQUEST_VALIDATORS].get(validator_name): # Adding only if the validator hasn't been defined already @@ -828,7 +873,7 @@ def add_request_validator_to_method(self, path, method_name, validate_body=False if not self.method_definition_has_integration(method_definition): continue - set_validator_to_method = {self._X_APIGW_REQUEST_VALIDATOR: validator_name} + set_validator_to_method = Py27Dict({self._X_APIGW_REQUEST_VALIDATOR: validator_name}) # Setting validator to the given method method_definition.update(set_validator_to_method) @@ -855,11 +900,12 @@ def add_request_model_to_method(self, path, method_name, request_model): existing_parameters = method_definition.get("parameters", []) - parameter = { - "in": "body", - "name": model_name, - "schema": {"$ref": "#/definitions/{}".format(model_name)}, - } + # construct parameter as py27 dict + # and insert keys one by one to preserve input order + parameter = Py27Dict() + parameter["in"] = "body" + parameter["name"] = model_name + parameter["schema"] = {"$ref": "#/definitions/{}".format(model_name)} if model_required is not None: parameter["required"] = model_required @@ -884,7 +930,7 @@ def add_gateway_responses(self, gateway_responses): :param dict gateway_responses: Dictionary of GatewayResponse configuration which gets translated. """ - self.gateway_responses = self.gateway_responses or {} + self.gateway_responses = self.gateway_responses or Py27Dict() for response_type, response in gateway_responses.items(): self.gateway_responses[response_type] = response.generate_swagger() @@ -897,7 +943,7 @@ def add_models(self, models): :return: """ - self.definitions = self.definitions or {} + self.definitions = self.definitions or Py27Dict() for model_name, schema in models.items(): @@ -961,6 +1007,7 @@ def add_resource_policy(self, resource_policy, path, stage): ] ) + # FIXME: check if this requires py27 dict? blacklist_dict = { "StringEndpointList": source_vpc_blacklist, "IntrinsicVpcList": source_vpc_intrinsic_blacklist, @@ -1013,11 +1060,11 @@ def _add_iam_resource_policy_for_method(self, policy_list, effect, resource_list policy_list = [policy_list] self.resource_policy["Version"] = "2012-10-17" - policy_statement = {} + policy_statement = Py27Dict() policy_statement["Effect"] = effect policy_statement["Action"] = "execute-api:Invoke" policy_statement["Resource"] = resource_list - policy_statement["Principal"] = {"AWS": policy_list} + policy_statement["Principal"] = Py27Dict({"AWS": policy_list}) if self.resource_policy.get("Statement") is None: self.resource_policy["Statement"] = policy_statement @@ -1042,6 +1089,9 @@ def _get_method_path_uri_list(self, path, stage): for m in methods: method = "*" if (m.lower() == self._X_ANY_METHOD or m.lower() == "any") else m.upper() resource = "execute-api:/${__Stage__}/" + method + path + resource = ( + Py27UniStr(resource) if isinstance(method, Py27UniStr) or isinstance(path, Py27UniStr) else resource + ) resource = fnSub(resource, {"__Stage__": stage}) uri_list.extend([resource]) return uri_list @@ -1062,13 +1112,13 @@ def _add_ip_resource_policy_for_method(self, ip_list, conditional, resource_list raise ValueError("Conditional must be one of {}".format(["IpAddress", "NotIpAddress"])) self.resource_policy["Version"] = "2012-10-17" - allow_statement = {} + allow_statement = Py27Dict() allow_statement["Effect"] = "Allow" allow_statement["Action"] = "execute-api:Invoke" allow_statement["Resource"] = resource_list allow_statement["Principal"] = "*" - deny_statement = {} + deny_statement = Py27Dict() deny_statement["Effect"] = "Deny" deny_statement["Action"] = "execute-api:Invoke" deny_statement["Resource"] = resource_list @@ -1097,7 +1147,7 @@ def _add_vpc_resource_policy_for_method(self, endpoint_dict, conditional, resour if conditional not in ["StringNotEquals", "StringEquals"]: raise ValueError("Conditional must be one of {}".format(["StringNotEquals", "StringEquals"])) - condition = {} + condition = Py27Dict() string_endpoint_list = endpoint_dict.get("StringEndpointList") intrinsic_vpc_endpoint_list = endpoint_dict.get("IntrinsicVpcList") intrinsic_vpce_endpoint_list = endpoint_dict.get("IntrinsicVpceList") @@ -1126,13 +1176,13 @@ def _add_vpc_resource_policy_for_method(self, endpoint_dict, conditional, resour return self.resource_policy["Version"] = "2012-10-17" - allow_statement = {} + allow_statement = Py27Dict() allow_statement["Effect"] = "Allow" allow_statement["Action"] = "execute-api:Invoke" allow_statement["Resource"] = resource_list allow_statement["Principal"] = "*" - deny_statement = {} + deny_statement = Py27Dict() deny_statement["Effect"] = "Deny" deny_statement["Action"] = "execute-api:Invoke" deny_statement["Resource"] = resource_list @@ -1201,7 +1251,13 @@ def add_request_parameters_to_method(self, path, method_name, request_parameters if location == "querystring": location = "query" - parameter = {"in": location, "name": name, "required": request_parameter["Required"], "type": "string"} + # create parameter as py27 dict + # and insert keys one by one to preserve input orders + parameter = Py27Dict() + parameter["in"] = location + parameter["name"] = name + parameter["required"] = request_parameter["Required"] + parameter["type"] = "string" existing_parameters.append(parameter) @@ -1223,7 +1279,10 @@ def swagger(self): """ # Make sure any changes to the paths are reflected back in output - self._doc["paths"] = self.paths + # iterate keys to make sure if "paths" is of Py27UniStr type, it won't be overriden as str + for key in self._doc.keys(): + if key == "paths": + self._doc[key] = self.paths if self.security_definitions: self._doc["securityDefinitions"] = self.security_definitions @@ -1284,7 +1343,13 @@ def gen_skeleton(): :return dict: Dictionary of a skeleton swagger document """ - return {"swagger": "2.0", "info": {"version": "1.0", "title": ref("AWS::StackName")}, "paths": {}} + skeleton = Py27Dict() + skeleton["swagger"] = "2.0" + skeleton["info"] = Py27Dict() + skeleton["info"]["version"] = "1.0" + skeleton["info"]["title"] = ref("AWS::StackName") + skeleton["paths"] = Py27Dict() + return skeleton @staticmethod def _get_authorization_scopes(authorizers, default_authorizer): @@ -1338,7 +1403,10 @@ def safe_compare_regex_with_string(regex, data): @staticmethod def get_path_without_trailing_slash(path): # convert greedy paths to such as {greedy+}, {proxy+} to "*" - return re.sub(r"{([a-zA-Z0-9._-]+|[a-zA-Z0-9._-]+\+|proxy\+)}", "*", path) + sub = re.sub(r"{([a-zA-Z0-9._-]+|[a-zA-Z0-9._-]+\+|proxy\+)}", "*", path) + if isinstance(path, Py27UniStr): + return Py27UniStr(sub) + return sub @staticmethod def get_validator_name(validate_body, validate_parameters): diff --git a/samtranslator/third_party/__init__.py b/samtranslator/third_party/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/samtranslator/third_party/py27hash/__init__.py b/samtranslator/third_party/py27hash/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/samtranslator/third_party/py27hash/hash.py b/samtranslator/third_party/py27hash/hash.py new file mode 100644 index 000000000..b71b18d4b --- /dev/null +++ b/samtranslator/third_party/py27hash/hash.py @@ -0,0 +1,169 @@ +""" +Compatibility methods to support Python 2.7 style hashing in Python 3.X+ + +This is designed for compatibility not performance. +""" + +import ctypes +import math + + +def hash27(value): + """ + Wrapper call to Hash.hash() + + Args: + value: input value + + Returns: + Python 2.7 hash + """ + + return Hash.hash(value) + + +class Hash(object): + """ + Various hashing methods using Python 2.7's algorithms + """ + + @staticmethod + def hash(value): + """ + Returns a Python 2.7 hash for a value. + + Args: + value: input value + + Returns: + Python 2.7 hash + """ + + if isinstance(value, tuple): + return Hash.thash(value) + if isinstance(value, float): + return Hash.fhash(value) + if isinstance(value, int): + return hash(value) + if isinstance(value, ("".__class__, bytes)) or type(value).__name__ == "buffer": + return Hash.shash(value) + + raise TypeError("unhashable type: '%s'" % (type(value).__name__)) + + @staticmethod + def thash(value): + """ + Returns a Python 2.7 hash for a tuple. + + Logic ported from the 2.7 Python branch: cpython/Objects/tupleobject.c + Method: static long tuplehash(PyTupleObject *v) + + Args: + value: input tuple + + Returns: + Python 2.7 hash + """ + + length = len(value) + + mult = 1000003 + + x = 0x345678 + for y in value: + length -= 1 + + y = Hash.hash(y) + x = (x ^ y) * mult + mult += 82520 + length + length + + x += 97531 + + if x == -1: + x = -2 + + # Convert to C type + return ctypes.c_long(x).value + + @staticmethod + def fhash(value): + """ + Returns a Python 2.7 hash for a float. + + Logic ported from the 2.7 Python branch: cpython/Objects/object.c + Method: long _Py_HashDouble(double v) + + Args: + value: input float + + Returns: + Python 2.7 hash + """ + + fpart = math.modf(value) + if fpart[0] == 0.0: + return hash(int(fpart[1])) + + v, e = math.frexp(value) + + # 2**31 + v *= 2147483648.0 + + # Top 32 bits + hipart = int(v) + + # Next 32 bits + v = (v - float(hipart)) * 2147483648.0 + + x = hipart + int(v) + (e << 15) + if x == -1: + x = -2 + + # Convert to C long type + return ctypes.c_long(x).value + + @staticmethod + def shash(value): + """ + Returns a Python 2.7 hash for a string. + + Logic ported from the 2.7 Python branch: cpython/Objects/stringobject.c + Method: static long string_hash(PyStringObject *a) + + Args: + value: input string + + Returns: + Python 2.7 hash + """ + + length = len(value) + + if length == 0: + return 0 + + x = Hash.ordinal(value[0]) << 7 + for c in value: + x = (1000003 * x) ^ Hash.ordinal(c) + + x ^= length + x &= 0xFFFFFFFFFFFFFFFF + if x == -1: + x = -2 + + # Convert to C long type + return ctypes.c_long(x).value + + @staticmethod + def ordinal(value): + """ + Converts value to an ordinal or returns the input value if it's an int. + + Args: + value: input + + Returns: + ordinal for value + """ + + return value if isinstance(value, int) else ord(value) diff --git a/samtranslator/translator/transform.py b/samtranslator/translator/transform.py index f3c564f39..8be5e6000 100644 --- a/samtranslator/translator/transform.py +++ b/samtranslator/translator/transform.py @@ -1,5 +1,6 @@ from samtranslator.translator.translator import Translator from samtranslator.parser.parser import Parser +from samtranslator.utils.py27hash_fix import to_py27_compatible_template, undo_mark_unicode_str_in_template def transform(input_fragment, parameter_values, managed_policy_loader, feature_toggle=None): @@ -12,5 +13,8 @@ def transform(input_fragment, parameter_values, managed_policy_loader, feature_t """ sam_parser = Parser() + to_py27_compatible_template(input_fragment, parameter_values) translator = Translator(managed_policy_loader.load(), sam_parser) - return translator.translate(input_fragment, parameter_values=parameter_values, feature_toggle=feature_toggle) + transformed = translator.translate(input_fragment, parameter_values=parameter_values, feature_toggle=feature_toggle) + transformed = undo_mark_unicode_str_in_template(transformed) + return transformed diff --git a/samtranslator/utils/py27hash_fix.py b/samtranslator/utils/py27hash_fix.py new file mode 100644 index 000000000..0172784ff --- /dev/null +++ b/samtranslator/utils/py27hash_fix.py @@ -0,0 +1,632 @@ +""" +""" + +import ctypes +import copy +import json +import sys +import logging + +from samtranslator.parser.parser import Parser +from samtranslator.third_party.py27hash.hash import Hash + + +LOG = logging.getLogger(__name__) +# Constants based on Python2.7 dictionary +# See: https://github.com/python/cpython/blob/v2.7.18/Objects/dictobject.c +MINSIZE = 8 +PERTURB_SHIFT = 5 + +unicode_string_type = str if sys.version_info.major >= 3 else unicode +long_int_type = int if sys.version_info.major >= 3 else long + + +def to_py27_compatible_template(template, parameter_values=None): + """ + Convert an input template to a py27hash-compatible template. This function has to be run before any + manipulation occurs for sake of keeping the same initial state. This function modifies the input template, + rather than return a copied template. We choose not to return a copy because copying the template might + change its internal state in Py2.7. + We only convert necessary parts in the template which could affect the hash generation for Serverless Api + template is modified + Also update parameter_values to py27hash-compatible if it is provided. + + Parameters + ---------- + template: dict + input template + + Returns + ------- + None + """ + # Passing to parser for a simple validation. Validation is normally done within translator.translate(...). + # However, becuase this conversion is done before translate and also requires the template to be valid, we + # perform a simple validation here to just make sure the template is minimally safe for conversion. + Parser.validate_datatypes(template) + + if not _template_has_api_resource(template) and not _template_has_httpapi_resource_with_default_authorizer( + template + ): + # no need to convert when all of the following conditions are true: + # 1. template does not contain any API resource + # 2. template does not contain any HttpApi resource with DefaultAuthorizer (TODO: remove after py3 migration and fix of security issue) + return + + if "Globals" in template and isinstance(template["Globals"], dict) and "Api" in template["Globals"]: + # "Api" section under "Globals" could affect swagger generation for AWS::Serverless::Api resources + template["Globals"]["Api"] = _convert_to_py27_type(template["Globals"]["Api"]) + + if "Parameters" in template and isinstance(template["Parameters"], dict): + new_parameters_dict = Py27Dict() + for logical_id, param_dict in template["Parameters"].items(): + if isinstance(param_dict, dict) and "Default" in param_dict: + param_dict["Default"] = _convert_to_py27_type(param_dict["Default"]) + + # dict keys have to be Py27UniStr for correct serialization + new_parameters_dict[Py27UniStr(logical_id)] = param_dict + template["Parameters"] = new_parameters_dict + + if "Resources" in template and isinstance(template["Resources"], dict): + new_resources_dict = Py27Dict() + for logical_id, resource_dict in template["Resources"].items(): + if isinstance(resource_dict, dict): + resource_type = resource_dict.get("Type") + resource_properties = resource_dict.get("Properties") + if resource_properties is not None: + # We only convert for AWS::Serverless::Api resource + if resource_type in [ + "AWS::Serverless::Api", + "AWS::Serverless::HttpApi", + ]: + resource_dict["Properties"] = _convert_to_py27_type(resource_properties) + elif resource_type in ["AWS::Serverless::Function", "AWS::Serverless::StateMachine"]: + # properties below could affect swagger generation + if "Condition" in resource_dict: + resource_dict["Condition"] = _convert_to_py27_type(resource_dict["Condition"]) + if "FunctionName" in resource_properties: + resource_properties["FunctionName"] = _convert_to_py27_type( + resource_properties["FunctionName"] + ) + if "Events" in resource_properties: + resource_properties["Events"] = _convert_to_py27_type(resource_properties["Events"]) + + new_resources_dict[Py27UniStr(logical_id)] = resource_dict + template["Resources"] = new_resources_dict + + if parameter_values: + for key, val in parameter_values.items(): + parameter_values[key] = _convert_to_py27_type(val) + + +def undo_mark_unicode_str_in_template(template_dict): + return json.loads(json.dumps(template_dict)) + + +class Py27UniStr(unicode_string_type): + """ + A string subclass to allow string be recognized as Python2 unicode string + To preserve the instance type in string operations, we need to override certain methods + """ + + def __add__(self, other): + return Py27UniStr(super(Py27UniStr, self).__add__(other)) + + def __repr__(self): + if sys.version_info.major >= 3: + return "u" + super(Py27UniStr, self).encode("unicode_escape").decode("ascii").__repr__().replace( + "\\\\", "\\" + ) + return super(Py27UniStr, self).__repr__() + + def upper(self): + return Py27UniStr(super(Py27UniStr, self).upper()) + + def lower(self): + return Py27UniStr(super(Py27UniStr, self).lower()) + + def replace(self, __old, __new, __count=None): + if __count: + return Py27UniStr(super(Py27UniStr, self).replace(__old, __new, __count)) + return Py27UniStr(super(Py27UniStr, self).replace(__old, __new)) + + def split(self, sep=None, maxsplit=-1): + return [Py27UniStr(s) for s in super(Py27UniStr, self).split(sep, maxsplit)] + + +class Py27LongInt(long_int_type): + """ + An int subclass to allow int be recognized as Python2 long int + Overriding __repr__ only + """ + + PY2_MAX_INT = 9223372036854775807 # sys.maxint from Python2.7 Lambda runtime + + def __repr__(self): + if sys.version_info.major >= 3 and self > Py27LongInt.PY2_MAX_INT: + return super(Py27LongInt, self).__repr__() + "L" + return super(Py27LongInt, self).__repr__() + + +class Py27Keys(object): + """ + A class for tracking keys based on based on Python 2.7 order. + Based on https://github.com/python/cpython/blob/v2.7.18/Objects/dictobject.c. + + The order of keys in Python 2.7 is path dependent -- the order of inserts and deletes matters + in determining the iteration order. + """ + + DUMMY = ["dummy"] # marker for deleted keys + + def __init__(self): + super(Py27Keys, self).__init__() + self.debug = False + self.keyorder = dict() + self.size = 0 # current size of the keys, equivalent to ma_used in dictobject.c + self.fill = 0 # increment count when a key is added, equivalent to ma_fill in dictobject.c + self.mask = MINSIZE - 1 # Python2 default dict size + + def _get_key_idx(self, k): + """Gets insert location for k""" + freeslot = None + # C API uses unsigned values + h = ctypes.c_size_t(Hash.hash(k)).value + i = h & self.mask + + if i not in self.keyorder or self.keyorder[i] == k: + # empty slot or keys match + return i + + if i in self.keyorder and self.keyorder[i] is self.DUMMY: + # dummy slot + freeslot = i + + walker = i + perturb = h + while i in self.keyorder and self.keyorder[i] != k: + walker = (walker << 2) + walker + perturb + 1 + i = walker & self.mask + + if i not in self.keyorder: + return i if freeslot is None else freeslot + if self.keyorder[i] == k: + return i + if freeslot is None and self.keyorder[i] is self.DUMMY: + freeslot = i + perturb >>= PERTURB_SHIFT + return i + + def _resize(self, request): + """ + Resizes allocated size based + """ + newsize = MINSIZE + while newsize <= request: + newsize <<= 1 + + self.mask = newsize - 1 + + # Reset key list to simulate the dict resize and copy operation + oldkeyorder = copy.copy(self.keyorder) + self.keyorder = dict() + self.fill = self.size = 0 + # reinsert all the keys using original order + for idx in sorted(oldkeyorder.keys()): + if oldkeyorder[idx] is not self.DUMMY: + self.add(oldkeyorder[idx]) + + def remove(self, key): + """Removes key""" + i = self._get_key_idx(key) + if i in self.keyorder: + if self.keyorder[i] is not self.DUMMY: + self.keyorder[i] = self.DUMMY + self.size -= 1 + + def add(self, key): + """Adds key""" + start_size = self.size + i = self._get_key_idx(key) + if i not in self.keyorder: + # We are not replacing an existing key or a DUMMY key, increment fill + self.size += 1 + self.fill += 1 + self.keyorder[i] = key + else: + if self.keyorder[i] is self.DUMMY: + self.size += 1 + if self.keyorder[i] != key: + self.keyorder[i] = key + + # Resize if 2/3 capacity + if self.size > start_size and self.fill * 3 >= ((self.mask + 1) * 2): + # Python2 dict increases size by a factor of 4 for small dict, and 2 for large dict + self._resize(self.size * (2 if self.size > 50000 else 4)) + + def keys(self): + """Return keys in Python2 order""" + return [self.keyorder[key] for key in sorted(self.keyorder.keys()) if self.keyorder[key] is not self.DUMMY] + + def __setstate__(self, state): + """ + Overrides default pickling object to force re-adding all keys and match Python 2.7 deserialization logic. + + :param state: input state + """ + self.__dict__ = state + keys = self.keys() + + # Clear keys and re-add to match deserialization logic + self.__init__() + + for k in keys: + if k == self.DUMMY: + continue + self.add(k) + + def __iter__(self): + """ + Default iterator + """ + return iter(self.keys()) + + def __eq__(self, other): + if isinstance(other, Py27Keys): + return self.keys() == other.keys() + if isinstance(other, list): + return self.keys() == other + return False + + def __len__(self): + return len(self.keys()) + + def merge(self, other): + """ + Merge keys from an exisitng iterable into this key list. + Equivalent to PyDict_Merge + + :param other: iterable + """ + if len(other) == 0 or self is other: + # nothing to do + return + + # PyDict_Merge initial merge size is double the size of current + incoming dict + if ((self.fill + len(other)) * 3) >= ((self.mask + 1) * 2): + self._resize((self.size + len(other)) * 2) + + # Copy actual keys + for k in other: + self.add(k) + + def copy(self): + """ + Makes a copy of self + """ + # Copy creates a new object and merges keys in + new = Py27Keys() + new.merge(self.keys()) + return new + + def pop(self): + """ + Pops the top element from the sorted keys if it exists. Returns None otherwise. + """ + if self.keyorder: + value = self.keys()[0] + self.remove(value) + return value + return None + + +class Py27Dict(dict): + """ + Compatibility class to support Python2.7 style iteration in Python3.x + """ + + def __init__(self, *args, **kwargs): + """ + Overrides dict logic to always call set item. This allows Python2.7 style iteration + """ + super(Py27Dict, self).__init__() + + # Initialize iteration key list + self.keylist = Py27Keys() + + # Initialize base arguments + self.update(*args, **kwargs) + + def __reduce__(self): + """ + Method necessary to fully pickle Python 3 subclassed dict objects with attribute fields. + """ + # pylint: disable = W0235 + return super(Py27Dict, self).__reduce__() + + def __setitem__(self, key, value): + """ + Override of __setitem__ to track keys and simulate Python2.7 dict + + Parameters + ---------- + key: hashable + value: Any + """ + super(Py27Dict, self).__setitem__(key, value) + self.keylist.add(key) + + def __delitem__(self, key): + """ + Override of __delitem__ to track kyes and simulate Python2.7 dict. + + Parameters + ---------- + key: hashable + """ + super(Py27Dict, self).__delitem__(key) + self.keylist.remove(key) + + def update(self, *args, **kwargs): + """ + Overrides dict logic to always call set item. This allows Python2.7 style iteration. + + Parameters + ---------- + args: args + kwargs: keyword args + """ + for arg in args: + # Cast to dict if applicable. Otherwise, assume it's an iterable of (key, value) pairs + if isinstance(arg, dict): + # Merge incoming keys into keylist + self.keylist.merge(arg.keys()) + arg = arg.items() + + for k, v in arg: + self[k] = v + + for k, v in dict(**kwargs).items(): + self[k] = v + + def clear(self): + """ + Clears the dict along with its backing Python2.7 keylist. + """ + super(Py27Dict, self).clear() + self.keylist = Py27Keys() + + def copy(self): + """ + Copies the dict along with its backing Python2.7 keylist. + + Returns + ------- + Py27Dict + copy of self + """ + new = Py27Dict() + + # First copy the keylist to the new object + new.keylist = self.keylist.copy() + + # Copy keys into backing dict + for k, v in self.items(): + new[k] = v + + return new + + def pop(self, key, default=None): + """ + Pops the value at key from the dict if it exists, return default otherwise + + Parameters + ---------- + key: hashable + key to remove + default: Any + value to return if key is not found + + Returns + ------- + Any + value of key if found or default + """ + value = super(Py27Dict, self).pop(key, default) + self.keylist.remove(key) + return value + + def popitem(self): + """ + Pops an element from the dict and returns the item. + + Returns + ------- + tuple + (key, value) pair of an element if found or None if dict is empty + """ + if self: + key = self.keylist.pop() + value = self[key] if key else None + + del self[key] + return key, value + + return None + + def __iter__(self): + """ + Default iterator + + Returns + ------- + iterator + """ + return self.keylist.__iter__() + + def __str__(self): + """ + Override to minic exact Python2.7 str(dict_obj) + + Returns + ------- + str + """ + string = "{" + + for i, key in enumerate(self): + string += ", " if i > 0 else "" + if isinstance(key, ("".__class__, bytes)): + string += "%s: " % key.__repr__() + else: + string += "%s: " % key + + if isinstance(self[key], ("".__class__, bytes)): + string += "%s" % self[key].__repr__() + else: + string += "%s" % self[key] + + string += "}" + return string + + def __repr__(self): + """ + Create a string version of this Dict + + Returns + ------- + str + """ + return self.__str__() + + def keys(self): + """ + Returns keys ordered using Python2.7 iteration alogrithm + + Returns + ------- + list + list of keys + """ + return self.keylist.keys() + + def values(self): + """ + Returns values ordered using Python2.7 iteration algorithm + + Returns + ------- + list + list of values + """ + return [self[k] for k in self.keys()] + + def items(self): + """ + Returns items ordered using Python2.7 iteration algorithm + + Returns + ------- + list + list of items + """ + return [(k, self[k]) for k in self.keys()] + + def setdefault(self, key, default): + """ + Retruns the value of a key if the key exists. Otherwise inserts key with the default value + + Parameters + ---------- + key: hashable + default: Any + + Returns + ------- + Any + """ + if key not in self: + self[key] = default + return self[key] + + +def _convert_to_py27_type(original): + if isinstance(original, ("".__class__, bytes)): + # these are strings, return the Py27UniStr instance of the string + return Py27UniStr(original) + + if isinstance(original, int) and original > Py27LongInt.PY2_MAX_INT: + # only convert long int to Py27LongInt + return Py27LongInt(original) + + if isinstance(original, list): + return [_convert_to_py27_type(item) for item in original] + + if isinstance(original, dict): + # Recursively convert dict items + key_list = original.keys() + new_dict = Py27Dict() + for key in key_list: + new_dict[Py27UniStr(key)] = _convert_to_py27_type(original[key]) + return new_dict + + # Anything else does not require conversion + return original + + +def _template_has_api_resource(template): + """ + Returns true if the template contains at lease one explicit or implicit AWS::Serverless::Api resource + """ + for resource_dict in template.get("Resources", {}).values(): + if isinstance(resource_dict, dict) and resource_dict.get("Type") == "AWS::Serverless::Api": + # i.e. an excplicit API is defined in the template + return True + + if isinstance(resource_dict, dict) and resource_dict.get("Type") in [ + "AWS::Serverless::Function", + "AWS::Serverless::StateMachine", + ]: + events = resource_dict.get("Properties", {}).get("Events", {}) + if isinstance(events, dict): + for event_dict in events.values(): + # An explicit or implicit API is referenced + if event_dict and isinstance(event_dict, dict) and event_dict.get("Type") == "Api": + return True + + return False + + +def _template_has_httpapi_resource_with_default_authorizer(template): + """ + Returns true if the template contains at least one AWS::Serverless::HttpApi resource with DefaultAuthorizer configured + """ + # Check whether DefaultAuthorizer is defined in Globals.HttpApi + has_global_httpapi_default_authorizer = False + if "Globals" in template and isinstance(template["Globals"], dict): + globals_dict = template["Globals"] + if "HttpApi" in globals_dict and isinstance(globals_dict["HttpApi"], dict): + globals_httpapi_dict = globals_dict["HttpApi"] + if "Auth" in globals_httpapi_dict and isinstance(globals_httpapi_dict["Auth"], dict): + has_global_httpapi_default_authorizer = bool(globals_httpapi_dict["Auth"].get("DefaultAuthorizer")) + + # Check if there is explicit HttpApi resource + for resource_dict in template.get("Resources", {}).values(): + if isinstance(resource_dict, dict) and resource_dict.get("Type") == "AWS::Serverless::HttpApi": + auth = resource_dict.get("Properties", {}).get("Auth", {}) + if ( + auth and isinstance(auth, dict) and auth.get("DefaultAuthorizer") + ) or has_global_httpapi_default_authorizer: + return True + + # Check if there is any httpapi event for implicit api + if has_global_httpapi_default_authorizer: + for resource_dict in template.get("Resources", {}).values(): + if isinstance(resource_dict, dict) and resource_dict.get("Type") == "AWS::Serverless::Function": + events = resource_dict.get("Properties", {}).get("Events", {}) + if isinstance(events, dict): + for event_dict in events.values(): + if event_dict and isinstance(event_dict, dict) and event_dict.get("Type") == "HttpApi": + return True + + return False diff --git a/setup.cfg b/setup.cfg index b88034e41..52a96f1e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [metadata] description-file = README.md +license_files = + LICENSE* + NOTICE* + THIRD_PARTY_LICENSES \ No newline at end of file diff --git a/setup.py b/setup.py index 8ca3f5605..2d304b1a4 100755 --- a/setup.py +++ b/setup.py @@ -61,6 +61,11 @@ def read_requirements(req="base.txt"): packages=find_packages( exclude=("tests", "tests.*", "integration", "integration.*", "docs", "examples", "versions") ), + license_files=( + "LICENSE", + "NOTICE", + "THIRD_PARTY_LICENSES", + ), install_requires=read_requirements("base.txt"), include_package_data=True, extras_require={"dev": read_requirements("dev.txt")}, @@ -74,7 +79,6 @@ def read_requirements(req="base.txt"): "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/tests/translator/input/function_with_null_events.yaml b/tests/translator/input/function_with_null_events.yaml new file mode 100644 index 000000000..3a49fa6ff --- /dev/null +++ b/tests/translator/input/function_with_null_events.yaml @@ -0,0 +1,8 @@ +Resources: + FunctionWithNullEvents: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/queues.zip + Handler: handlers.handler + Runtime: python3.8 + Events: diff --git a/tests/translator/input/state_machine_with_null_events.yaml b/tests/translator/input/state_machine_with_null_events.yaml new file mode 100644 index 000000000..5c74098c3 --- /dev/null +++ b/tests/translator/input/state_machine_with_null_events.yaml @@ -0,0 +1,7 @@ +Resources: + StateMachine: + Type: 'AWS::Serverless::StateMachine' + Properties: + DefinitionUri: s3://sam-demo-bucket/my_state_machine.asl.json + Role: arn:aws:iam::123456123456:role/service-role/SampleRole + Events: \ No newline at end of file diff --git a/tests/translator/output/aws-cn/function_with_null_events.json b/tests/translator/output/aws-cn/function_with_null_events.json new file mode 100644 index 000000000..4983f3fc4 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_null_events.json @@ -0,0 +1,57 @@ +{ + "Resources": { + "FunctionWithNullEvents": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "queues.zip" + }, + "Handler": "handlers.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionWithNullEventsRole", + "Arn" + ] + }, + "Runtime": "python3.8", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "FunctionWithNullEventsRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/state_machine_with_null_events.json b/tests/translator/output/aws-cn/state_machine_with_null_events.json new file mode 100644 index 000000000..eb62a9041 --- /dev/null +++ b/tests/translator/output/aws-cn/state_machine_with_null_events.json @@ -0,0 +1,20 @@ +{ + "Resources": { + "StateMachine": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionS3Location": { + "Bucket": "sam-demo-bucket", + "Key": "my_state_machine.asl.json" + }, + "RoleArn": "arn:aws:iam::123456123456:role/service-role/SampleRole", + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/function_with_null_events.json b/tests/translator/output/aws-us-gov/function_with_null_events.json new file mode 100644 index 000000000..80f176adf --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_null_events.json @@ -0,0 +1,57 @@ +{ + "Resources": { + "FunctionWithNullEvents": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "queues.zip" + }, + "Handler": "handlers.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionWithNullEventsRole", + "Arn" + ] + }, + "Runtime": "python3.8", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "FunctionWithNullEventsRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/state_machine_with_null_events.json b/tests/translator/output/aws-us-gov/state_machine_with_null_events.json new file mode 100644 index 000000000..eb62a9041 --- /dev/null +++ b/tests/translator/output/aws-us-gov/state_machine_with_null_events.json @@ -0,0 +1,20 @@ +{ + "Resources": { + "StateMachine": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionS3Location": { + "Bucket": "sam-demo-bucket", + "Key": "my_state_machine.asl.json" + }, + "RoleArn": "arn:aws:iam::123456123456:role/service-role/SampleRole", + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/function_with_null_events.json b/tests/translator/output/function_with_null_events.json new file mode 100644 index 000000000..8623c35a8 --- /dev/null +++ b/tests/translator/output/function_with_null_events.json @@ -0,0 +1,57 @@ +{ + "Resources": { + "FunctionWithNullEvents": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "queues.zip" + }, + "Handler": "handlers.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionWithNullEventsRole", + "Arn" + ] + }, + "Runtime": "python3.8", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "FunctionWithNullEventsRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/state_machine_with_null_events.json b/tests/translator/output/state_machine_with_null_events.json new file mode 100644 index 000000000..eb62a9041 --- /dev/null +++ b/tests/translator/output/state_machine_with_null_events.json @@ -0,0 +1,20 @@ +{ + "Resources": { + "StateMachine": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionS3Location": { + "Bucket": "sam-demo-bucket", + "Key": "my_state_machine.asl.json" + }, + "RoleArn": "arn:aws:iam::123456123456:role/service-role/SampleRole", + "Tags": [ + { + "Key": "stateMachine:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 914e4a55f..577ad266e 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -3,6 +3,7 @@ import os.path import hashlib import sys +import re from functools import reduce, cmp_to_key from samtranslator.translator.translator import Translator, prepare_plugins, make_policy_template_for_function_plugin @@ -175,11 +176,6 @@ def _compare_transform(self, manifest, expected, partition, region): print(json.dumps(output_fragment, indent=2)) - # Only update the deployment Logical Id hash in Py3. - if sys.version_info.major >= 3: - self._update_logical_id_hash(expected) - self._update_logical_id_hash(output_fragment) - self.assertEqual(deep_sort_lists(output_fragment), deep_sort_lists(expected)) def _update_logical_id_hash(self, resources): @@ -379,6 +375,7 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): "function_with_global_layers", "function_with_layers", "function_with_many_layers", + "function_with_null_events", "function_with_permissions_boundary", "function_with_policy_templates", "function_with_sns_event_source_all_parameters", @@ -447,6 +444,7 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): "state_machine_with_condition_and_events", "state_machine_with_xray_policies", "state_machine_with_xray_role", + "state_machine_with_null_events", "function_with_file_system_config", "state_machine_with_permissions_boundary", "version_deletion_policy_precedence", @@ -559,11 +557,6 @@ def test_transform_success_openapi3(self, testcase, partition_with_region): print(json.dumps(output_fragment, indent=2)) - # Only update the deployment Logical Id hash in Py3. - if sys.version_info.major >= 3: - self._update_logical_id_hash(expected) - self._update_logical_id_hash(output_fragment) - self.assertEqual(deep_sort_lists(output_fragment), deep_sort_lists(expected)) @parameterized.expand( @@ -618,11 +611,6 @@ def test_transform_success_resource_policy(self, testcase, partition_with_region output_fragment = transform(manifest, parameter_values, mock_policy_loader) print(json.dumps(output_fragment, indent=2)) - # Only update the deployment Logical Id hash in Py3. - if sys.version_info.major >= 3: - self._update_logical_id_hash(expected) - self._update_logical_id_hash(output_fragment) - self.assertEqual(deep_sort_lists(output_fragment), deep_sort_lists(expected)) @parameterized.expand( @@ -690,6 +678,7 @@ def test_transform_invalid_document(testcase): transform(manifest, parameter_values, mock_policy_loader) error_message = get_exception_error_message(e) + error_message = re.sub(r"u'([A-Za-z0-9]*)'", r"'\1'", error_message) assert error_message == expected.get("errorMessage") @@ -1044,4 +1033,21 @@ def get_resource_by_type(template, type): def get_exception_error_message(e): - return reduce(lambda message, error: message + " " + error.message, e.value.causes, e.value.message) + return reduce( + lambda message, error: message + " " + error.message, + sorted(e.value.causes, key=_exception_sort_key), + e.value.message, + ) + + +def _exception_sort_key(cause): + """ + Returns the key to be used for sorting among other exceptions + """ + if hasattr(cause, "_logical_id"): + return cause._logical_id + if hasattr(cause, "_event_id"): + return cause._event_id + if hasattr(cause, "message"): + return cause.message + return str(cause) diff --git a/tests/utils/test_py27hash_fix.py b/tests/utils/test_py27hash_fix.py new file mode 100644 index 000000000..8d59aa940 --- /dev/null +++ b/tests/utils/test_py27hash_fix.py @@ -0,0 +1,838 @@ +import copy + +from unittest import TestCase +from mock import patch +from samtranslator.utils.py27hash_fix import ( + Py27Dict, + Py27Keys, + Py27UniStr, + Py27LongInt, + _convert_to_py27_type, + to_py27_compatible_template, + _template_has_api_resource, +) +from samtranslator.model.exceptions import InvalidDocumentException + + +class TestPy27UniStr(TestCase): + def test_equality(self): + original_str = "Hello, World!" + py27_str = Py27UniStr(original_str) + self.assertEqual(original_str, py27_str) + + def test_add_both_py27unistr(self): + part1 = Py27UniStr("foo") + part2 = Py27UniStr("bar") + added = part1 + part2 + self.assertIsInstance(added, Py27UniStr) + self.assertEqual(added, "foobar") + + def test_add_py27unistr_and_normal_str(self): + part1 = Py27UniStr("foo") + part2 = "bar" + added1 = part1 + part2 + self.assertIsInstance(added1, Py27UniStr) + self.assertEqual(added1, "foobar") + + added2 = part2 + part1 + self.assertIsInstance(added2, ("".__class__, bytes)) + self.assertNotIsInstance(added2, Py27UniStr) + self.assertEqual(added2, "barfoo") + + def test_repr(self): + py27str = Py27UniStr("some string") + self.assertEqual(repr(py27str), "u'some string'") + + def test_repr_with_unicode_literals(self): + py27str = Py27UniStr("\xdf\u9054") + self.assertEqual(repr(py27str), "u'\\xdf\\u9054'") + + def test_serialized_dict_with_unicode_literal_values(self): + d = {"key": Py27UniStr("\xdf")} + self.assertEqual(str(d), "{'key': u'\\xdf'}") + + def test_upper(self): + py27str = Py27UniStr("upper") + upper = py27str.upper() + self.assertIsInstance(upper, Py27UniStr) + self.assertEqual(upper, "UPPER") + + def test_lower(self): + py27str = Py27UniStr("LOWER") + lower = py27str.lower() + self.assertIsInstance(lower, Py27UniStr) + self.assertEqual(lower, "lower") + + def test_replace_wihtout_count(self): + py27str = Py27UniStr("aaa_bbb_aaa") + replaced = py27str.replace("b", "a") + self.assertIsInstance(replaced, Py27UniStr) + self.assertEqual(replaced, "aaa_aaa_aaa") + + def test_replace_with_count(self): + py27str = Py27UniStr("a_bb_a") + replaced = py27str.replace("b", "c", 1) + self.assertIsInstance(replaced, Py27UniStr) + self.assertEqual(replaced, "a_cb_a") + + def test_split_without_maxsplit(self): + before = Py27UniStr("a,b,c") + after = before.split(",") + self.assertIsInstance(after, list) + self.assertEqual(after, ["a", "b", "c"]) + for c in after: + self.assertIsInstance(c, Py27UniStr) + + def test_split_with_maxsplit(self): + before = Py27UniStr("a,b,c") + after = before.split(",", 1) + self.assertIsInstance(after, list) + self.assertEqual(after, ["a", "b,c"]) + for c in after: + self.assertIsInstance(c, Py27UniStr) + + +class TestPy27LongInt(TestCase): + def test_long_int(self): + i = Py27LongInt(9223372036854775810) + self.assertEqual(repr(i), "9223372036854775810L") + + def test_normal_int(self): + i = Py27LongInt(100) + self.assertEqual(repr(i), "100") + + def test_serialized_dict_with_long_int(self): + i = Py27LongInt(9223372036854775810) + d = {"num": i} + self.assertEqual(str(d), "{'num': 9223372036854775810L}") + + def test_serialized_dict_with_normal_int(self): + i = Py27LongInt(100) + d = {"num": i} + self.assertEqual(str(d), "{'num': 100}") + + +class TestPy27Keys(TestCase): + def test_merge(self): + input_keys_1 = ["a", "b", "c", "d"] + input_keys_2 = ["d", "e", "f", "g"] + py27_keys = Py27Keys() + for key in input_keys_1: + py27_keys.add(key) + self.assertEqual(py27_keys.keys(), ["a", "c", "b", "d"]) + py27_keys.merge(input_keys_2) + self.assertEqual(py27_keys.keys(), ["a", "c", "b", "e", "d", "g", "f"]) + + def test_copy(self): + input_keys = ["a", "b", "c", "d"] + py27_keys = Py27Keys() + for key in input_keys: + py27_keys.add(key) + + copied = py27_keys.copy() + self.assertEqual(copied.keys(), ["a", "c", "b", "d"]) + + def test_pop(self): + input_keys = ["a", "b", "c", "d"] + py27_keys = Py27Keys() + for key in input_keys: + py27_keys.add(key) + + self.assertEqual(py27_keys.pop(), "a") + + +class TestPy27Dict(TestCase): + def test_py27_iteration_order_01(self): + input_order = [ + ("/users", "get"), + ("/users", "post"), + ("/any/lambdatokennone", "any"), + ("/any/cognitomultiple", "any"), + ("/any/lambdatoken", "any"), + ("/any/default", "any"), + ("/any/lambdarequest", "any"), + ("/users", "patch"), + ("/users", "delete"), + ("/users", "put"), + ("/", "get"), + ("/any/noauth", "any"), + ] + expected_orders = [ + "/any/cognitomultiple", + "/any/lambdarequest", + "/any/default", + "/any/lambdatoken", + "/any/lambdatokennone", + "/", + "/any/noauth", + "/users", + ] + expected_copied_orders = [[0, 1, 2, 3, 4, 5, 6, 7]] + + py27_dict = Py27Dict() + for path, _ in input_order: + py27_dict[path] = "" + + self._validate_iteration_order(py27_dict, expected_orders, expected_copied_orders) + + def test_py27_iteration_order_02(self): + input_order = [ + "MyCognitoAuth", + "MyLambdaTokenAuthNoneFunctionInvokeRole", + "MyCognitoAuthMultipleUserPools", + "MyLambdaTokenAuth", + "MyLambdaRequestAuth", + ] + expected_order = [ + "MyCognitoAuth", + "MyLambdaTokenAuthNoneFunctionInvokeRole", + "MyCognitoAuthMultipleUserPools", + "MyLambdaTokenAuth", + "MyLambdaRequestAuth", + ] + expected_copied_orders = [ + [0, 1, 2, 3, 4], + ] + + py27_dict = Py27Dict() + for key in input_order: + py27_dict[key] = "" + + self._validate_iteration_order(py27_dict, expected_order, expected_copied_orders) + + def test_py27_iteration_order_03(self): + input_order = [ + "MyCognitoAuth", + "MyLambdaTokenAuthNoneFunctionInvokeRole", + "MyCognitoAuthMultipleUserPools", + "MyLambdaTokenAuth", + "MyLambdaRequestAuth", + "api_key", + ] + expected_order = [ + "MyLambdaTokenAuthNoneFunctionInvokeRole", + "api_key", + "MyLambdaRequestAuth", + "MyCognitoAuth", + "MyLambdaTokenAuth", + "MyCognitoAuthMultipleUserPools", + ] + expected_copied_orders = [ + [0, 5, 2, 3, 4, 1], + [0, 1, 2, 3, 4, 5], + [0, 5, 2, 3, 4, 1], + ] + + py27_dict = Py27Dict() + for key in input_order: + py27_dict[key] = "" + + self._validate_iteration_order(py27_dict, expected_order, expected_copied_orders) + + def test_py27_iteration_order_04(self): + """ + Variant of 03 + """ + input_order = [ + "MyCognitoAuth", + "MyLambdaTokenAuthNoneFunctionInvokeRole", + "MyCognitoAuthMultipleUserPools", + "MyLambdaTokenAuth", + "MyLambdaRequestAuth", + "api_key", + ] + expected_order = [ + "MyLambdaTokenAuthNoneFunctionInvokeRole", + "api_key", + "MyLambdaRequestAuth", + "MyCognitoAuth", + "MyLambdaTokenAuth", + "MyCognitoAuthMultipleUserPools", + ] + expected_copied_orders = [ + [0, 5, 2, 3, 4, 1], + [0, 1, 2, 3, 4, 5], + [0, 5, 2, 3, 4, 1], + ] + + py27_dict = Py27Dict() + for key in input_order[:-1]: + py27_dict[key] = "" + py27_dict = copy.deepcopy(py27_dict) + self.assertNotIn("api_key", py27_dict) + + py27_dict["api_key"] = "" + + self._validate_iteration_order(py27_dict, expected_order, expected_copied_orders) + + def test_py27_iteration_order_05(self): + """ + Variant of 04 - use .update() instead just setting the key + """ + input_order = [ + "MyCognitoAuth", + "MyLambdaTokenAuthNoneFunctionInvokeRole", + "MyCognitoAuthMultipleUserPools", + "MyLambdaTokenAuth", + "MyLambdaRequestAuth", + "api_key", + ] + expected_order = [ + "MyLambdaTokenAuthNoneFunctionInvokeRole", + "api_key", + "MyLambdaTokenAuth", + "MyLambdaRequestAuth", + "MyCognitoAuth", + "MyCognitoAuthMultipleUserPools", + ] + expected_copied_orders = [ + [0, 5, 3, 4, 2, 1], + [0, 1, 3, 4, 2, 5], + [0, 5, 3, 4, 2, 1], + ] + + py27_dict = Py27Dict() + for key in input_order[:-1]: + py27_dict[key] = "" + py27_dict = copy.deepcopy(py27_dict) + self.assertNotIn("api_key", py27_dict) + + py27_dict.update({"api_key": ""}) + + self._validate_iteration_order(py27_dict, expected_order, expected_copied_orders) + + def test_py27_iteration_order_06(self): + """ + A more complex test which simulate multiple dict operations + """ + py27_dict = Py27Dict() + py27_dict["info"] = "" + py27_dict["paths"] = {} + py27_dict["openapi"] = "3.1.1" + self.assertEqual(py27_dict.keys(), ["info", "paths", "openapi"]) + + py27_dict["securityDefinitions"] = {} + self.assertEqual(py27_dict.keys(), ["info", "paths", "securityDefinitions", "openapi"]) + + py27_dict = copy.deepcopy(py27_dict) + self.assertEqual(py27_dict.keys(), ["info", "paths", "openapi", "securityDefinitions"]) + + py27_dict["components"] = {} + self.assertEqual(py27_dict.keys(), ["info", "paths", "openapi", "components", "securityDefinitions"]) + + del py27_dict["securityDefinitions"] + self.assertEqual(py27_dict.keys(), ["info", "paths", "openapi", "components"]) + + py27_dict["securityDefinitions"] = {} + self.assertEqual(py27_dict.keys(), ["info", "paths", "openapi", "components", "securityDefinitions"]) + + del py27_dict["securityDefinitions"] + self.assertEqual(py27_dict.keys(), ["info", "paths", "openapi", "components"]) + + py27_dict = copy.deepcopy(py27_dict) + self.assertEqual(py27_dict.keys(), ["info", "paths", "components", "openapi"]) + + def test_py27_iteration_order_07(self): + """""" + input_order = ["get", "post", "patch", "delete", "put"] + expected_order = ["put", "delete", "post", "patch", "get"] + expected_copied_orders = [ + [0, 2, 3, 4, 1], + [0, 1, 2, 4, 3], + [0, 3, 2, 4, 1], + [0, 1, 2, 4, 3], + ] + py27_dict = Py27Dict() + py27_dict_setdefault = Py27Dict() + for key in input_order: + py27_dict[key] = {} + py27_dict_setdefault.setdefault(key, {}) + + self.assertEqual(py27_dict, py27_dict_setdefault) + self.assertEqual(py27_dict.keys(), py27_dict_setdefault.keys()) + + self._validate_iteration_order(py27_dict, expected_order, expected_copied_orders) + self._validate_iteration_order(py27_dict_setdefault, expected_order, expected_copied_orders) + + def _validate_iteration_order(self, py27_dict, expected_order, expected_copied_orders=[]): + """ + Validates iteration order of a Py27Dict with given input_order + If expected_copied_orders is supplied, validate also the iteration order after each deepcopy + """ + self.assertEqual(py27_dict.keys(), expected_order) + + for expected_copied_order in expected_copied_orders: + py27_dict = copy.deepcopy(py27_dict) + key_idx_order = [expected_order.index(i) for i in py27_dict.keys()] + self.assertEqual(key_idx_order, expected_copied_order) + + def test_dict_with_any_hashable_keys(self): + py27_dict = Py27Dict() + py27_dict["a"] = "b" + py27_dict[1] = 2 + py27_dict[1.1] = 2.2 + py27_dict[("c", "d")] = "" + self.assertEqual(py27_dict, {"a": "b", 1: 2, 1.1: 2.2, ("c", "d"): ""}) + + def test_dict_create_fail_with_unhashable_key(self): + with self.assertRaises(TypeError): + py27_dict = Py27Dict() + py27_dict[[1, 2]] = "" + + def test_dict_str_output(self): + py27_dict = Py27Dict() + py27_dict["a"] = "b" + self.assertEqual(str(py27_dict), "{'a': 'b'}") + self.assertEqual(py27_dict.__repr__(), "{'a': 'b'}") + + def test_dict_str_output_nested_dict(self): + py27_dict = Py27Dict({"a": {"b": "c"}, 1: ""}) + self.assertEqual(str(py27_dict), "{'a': {'b': 'c'}, 1: ''}") + self.assertEqual(py27_dict.__repr__(), "{'a': {'b': 'c'}, 1: ''}") + + def test_dict_py27unistr_output(self): + py27_dict = Py27Dict() + py27_dict[Py27UniStr("a")] = Py27UniStr("b") + self.assertEqual(str(py27_dict), "{u'a': u'b'}") + self.assertEqual(py27_dict.__repr__(), "{u'a': u'b'}") + + def test_dict_len(self): + py27_dict = Py27Dict({"a": "", "b": "", "c": ""}) + self.assertEqual(len(py27_dict), 3) + + def test_update_dict_with_dict(self): + py27_dict = Py27Dict({"a": ""}) + py27_dict.update({"b": ""}) + self.assertEqual(py27_dict, {"a": "", "b": ""}) + + py27_dict.update({}) + self.assertEqual(py27_dict, {"a": "", "b": ""}) + + def test_update_dict_with_tuple(self): + py27_dict = Py27Dict({"a": ""}) + py27_dict.update([("b", "")]) + self.assertEqual(py27_dict, {"a": "", "b": ""}) + + def test_update_dict_with_kwargs(self): + py27_dict = Py27Dict({"a": ""}) + py27_dict.update(c="d", foo="bar") + self.assertEqual(py27_dict, {"a": "", "c": "d", "foo": "bar"}) + + def test_clear_dict(self): + py27_dict = Py27Dict({"a": ""}) + py27_dict.clear() + self.assertEqual(py27_dict, {}) + + def test_copy_dict(self): + py27_dict = Py27Dict({"a": ""}) + self.assertEqual(py27_dict.copy(), {"a": ""}) + + def test_pop(self): + py27_dict = Py27Dict({"a": "b"}) + self.assertEqual(py27_dict.pop("a"), "b") + self.assertEqual(py27_dict.pop("c", "some_default_val"), "some_default_val") + + def test_popitem(self): + py27_dict = Py27Dict({"a": "b"}) + self.assertEqual(py27_dict.popitem(), ("a", "b")) + self.assertEqual(py27_dict.popitem(), None) + + def test_values(self): + py27_dict = Py27Dict({"a": "b", "c": "d"}) + self.assertEqual(py27_dict.values(), ["b", "d"]) + + def test_setdefault(self): + py27_dict = Py27Dict({"a": "b"}) + # Existing key + self.assertEqual(py27_dict.setdefault("a", "c"), "b") + # Non-existent key + self.assertEqual(py27_dict.setdefault("d", "c"), "c") + self.assertEqual(py27_dict, {"a": "b", "d": "c"}) + + +class TestConvertToPy27Dict(TestCase): + def test_with_string_input(self): + original = "aaa" + converted = _convert_to_py27_type(original) + self.assertIsInstance(converted, Py27UniStr) + self.assertEqual(converted, "aaa") + + def test_with_simple_dict(self): + original = {"a": "b"} + converted = _convert_to_py27_type(original) + self.assertIsInstance(converted, Py27Dict) + self.assertEqual(converted, {"a": "b"}) + self.assertEqual(str(converted), "{u'a': u'b'}") + + for key, val in converted.items(): + if isinstance(key, str): + self.assertIsInstance(key, Py27UniStr) + if isinstance(val, str): + self.assertIsInstance(val, Py27UniStr) + + def test_with_nested_dict(self): + original = {"a": {"b": "c"}} + converted = _convert_to_py27_type(original) + self.assertIsInstance(converted, Py27Dict) + self.assertIsInstance(converted["a"], Py27Dict) + + def test_with_list(self): + original = [{"a": "b"}, {"c": "d"}] + converted = _convert_to_py27_type(original) + self.assertIsInstance(converted, list) + for item in converted: + self.assertIsInstance(item, Py27Dict) + + def test_with_other_type(self): + original = [("a", "b"), set(["a", "b"]), 123, 123.123] + converted = _convert_to_py27_type(original) + self.assertIsInstance(converted[0], tuple) + self.assertIsInstance(converted[1], set) + self.assertIsInstance(converted[2], int) + self.assertIsInstance(converted[3], float) + self.assertEqual(original, converted) + + def test_with_long_int_input(self): + original = 9223372036854775810 + converted = _convert_to_py27_type(original) + self.assertIsInstance(converted, Py27LongInt) + self.assertEqual(converted, original) + + def test_with_normal_int_input(self): + original = 99 + converted = _convert_to_py27_type(original) + self.assertNotIsInstance(converted, Py27LongInt) + self.assertIsInstance(converted, int) + self.assertEqual(converted, original) + + +class TestToPy27CompatibleTemplate(TestCase): + def test_all(self): + input_template = { + "Globals": {"Api": {}}, + "Parameters": {"Param1": {"Default": "Value"}, "Param2": {"Default": {}}}, + "Resources": { + "Api": {"Type": "AWS::Serverless::Api", "Properties": {}}, + "Function": {"Type": "AWS::Serverless::Function", "Properties": {}}, + "StateMachine": {"Type": "AWS::Serverless::StateMachine", "Properties": {}}, + "Other": {"Type": "AWS::S3::Bucket", "Properties": {}}, + }, + } + param_values = {"paramA": "valueA", "paramB": ["valueB1", "valueB2", "valueB3"]} + to_py27_compatible_template(input_template, param_values) + self.assertEqual(str(input_template["Globals"]), "{'Api': {}}") + self.assertEqual( + str(input_template["Parameters"]), "{u'Param2': {'Default': {}}, u'Param1': {'Default': u'Value'}}" + ) + self.assertEqual( + str(input_template["Resources"]), + "{u'Function': {'Type': 'AWS::Serverless::Function', 'Properties': {}}, u'Api': {'Type': '" + "AWS::Serverless::Api', 'Properties': {}}, u'Other': {'Type': 'AWS::S3::Bucket', 'Properti" + "es': {}}, u'StateMachine': {'Type': 'AWS::Serverless::StateMachine', 'Properties': {}}}", + ) + self.assertEqual(str(param_values), "{'paramA': u'valueA', 'paramB': [u'valueB1', u'valueB2', u'valueB3']}") + + def test_empty_dict_fails_validation(self): + input_template = {} + with self.assertRaises(InvalidDocumentException): + to_py27_compatible_template(input_template) + + def test_only_globals_fails_validation(self): + input_template = { + "Globals": { + "Api": {"Name": "123"}, + "Function": {"Handler": "handler.handler"}, + } + } + with self.assertRaises(InvalidDocumentException): + to_py27_compatible_template(input_template) + + def test_only_parameters_fails_validation(self): + template = { + "Parameters": { + "Param1": {"Description": "description", "Default": "default value"}, + "Param2": {"Description": "description"}, + } + } + with self.assertRaises(InvalidDocumentException): + to_py27_compatible_template(template) + + def test_resources_api(self): + template = { + "Resources": { + "Api": {"Type": "AWS::Serverless::Api", "Properties": {"Name": "MyApi"}}, + "HttpApi": {"Type": "AWS::Serverless::HttpApi"}, + "Function": { + "Type": "AWS::Serverless::Function", + "Properties": { + "FunctionName": {"Ref": "MyFunctionName"}, + "Events": { + "ApiEvent": {"Type": "Api", "Properties": {"Path": "/user", "Method": "GET"}}, + "SecondApiEvent": {"Type": "Api", "Properties": {"Path": "/admin", "Method": "GET"}}, + }, + }, + }, + "StateMachine": { + "Type": "AWS::Serverless::StateMachine", + "Condition": "ShouldAddStateMachine", + "Properties": { + "Event": { + "ApiEvent": {"Type": "Api", "Properties": {"Path": "/state-machine", "Method": "GET"}} + } + }, + }, + } + } + to_py27_compatible_template(template) + self.assertIsInstance(template["Resources"], Py27Dict) + self.assertNotIsInstance(template["Resources"]["Api"], Py27Dict) + self.assertIsInstance(template["Resources"]["Api"]["Properties"], Py27Dict) + self.assertIsInstance(template["Resources"]["Api"]["Properties"]["Name"], Py27UniStr) + + def test_comprehensive_resources(self): + template = { + "Resources": { + "Api": {"Type": "AWS::Serverless::Api", "Properties": {"Name": "MyApi"}}, + "HttpApi": {"Type": "AWS::Serverless::HttpApi"}, + "Function": { + "Type": "AWS::Serverless::Function", + "Properties": { + "FunctionName": {"Ref": "MyFunctionName"}, + "Events": { + "ApiEvent": {"Type": "Api", "Properties": {"Path": "/user", "Method": "GET"}}, + "SecondApiEvent": {"Type": "Api", "Properties": {"Path": "/admin", "Method": "GET"}}, + }, + }, + }, + "StateMachine": { + "Type": "AWS::Serverless::StateMachine", + "Condition": "ShouldAddStateMachine", + "Properties": { + "Name": "statemachine", + "Events": { + "ApiEvent": {"Type": "Api", "Properties": {"Path": "/state-machine", "Method": "GET"}} + }, + }, + }, + } + } + to_py27_compatible_template(template) + + self.assertNotIsInstance(template, Py27Dict) + self.assertIsInstance(template["Resources"], Py27Dict) + + self.assertNotIsInstance(template["Resources"]["Api"], Py27Dict) + self.assertIsInstance(template["Resources"]["Api"]["Properties"], Py27Dict) + self.assertIsInstance(template["Resources"]["Api"]["Properties"]["Name"], Py27UniStr) + + self.assertNotIsInstance(template["Resources"]["HttpApi"], Py27Dict) + + self.assertNotIsInstance(template["Resources"]["Function"], Py27Dict) + self.assertNotIsInstance(template["Resources"]["Function"]["Properties"], Py27Dict) + self.assertIsInstance(template["Resources"]["Function"]["Properties"]["FunctionName"], Py27Dict) + self.assertIsInstance(template["Resources"]["Function"]["Properties"]["Events"], Py27Dict) + + self.assertNotIsInstance(template["Resources"]["StateMachine"], Py27Dict) + self.assertNotIsInstance(template["Resources"]["StateMachine"]["Properties"], Py27Dict) + self.assertIsInstance(template["Resources"]["StateMachine"]["Condition"], Py27UniStr) + self.assertNotIsInstance(template["Resources"]["StateMachine"]["Properties"]["Name"], Py27UniStr) + self.assertIsInstance(template["Resources"]["StateMachine"]["Properties"]["Events"], Py27Dict) + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_no_conversion_happens(self, _convert_to_py27_type_mock): + template = {"Resources": {"S3Bucket": {"Type": "AWS::S3::Bucket", "Properties": {}}}} + to_py27_compatible_template(template) + + _convert_to_py27_type_mock.assert_not_called() + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_explicit_api(self, _convert_to_py27_type_mock): + template = { + "Resources": { + "Api": {"Type": "AWS::Serverless::Api", "Properties": {"Name": "MyApi"}}, + } + } + to_py27_compatible_template(template) + + _convert_to_py27_type_mock.assert_called_once_with({"Name": "MyApi"}) + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_implicit_api(self, _convert_to_py27_type_mock): + template = { + "Resources": { + "Function": { + "Type": "AWS::Serverless::Function", + "Properties": { + "FunctionName": {"Ref": "MyFunctionName"}, + "Events": { + "ApiEvent": {"Type": "Api", "Properties": {"Path": "/user", "Method": "GET"}}, + "SecondApiEvent": {"Type": "Api", "Properties": {"Path": "/admin", "Method": "GET"}}, + }, + }, + }, + } + } + to_py27_compatible_template(template) + self.assertEqual(_convert_to_py27_type_mock.call_count, 2) + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_invalid_function_events(self, _convert_to_py27_type_mock): + template = { + "Resources": { + "Function": { + "Type": "AWS::Serverless::Function", + "Properties": { + "FunctionName": {"Ref": "MyFunctionName"}, + "Events": {"Fn::If": ["Condition", {}, {}]}, + }, + }, + } + } + to_py27_compatible_template(template) + _convert_to_py27_type_mock.assert_not_called() + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_explit_httpapi_with_default_authorizer(self, _convert_to_py27_type_mock): + template = { + "Resources": { + "Api": { + "Type": "AWS::Serverless::HttpApi", + "Properties": { + "Stage": "myStage", + "Auth": {"Authorizers": {"Authorizer1": {}}, "DefaultAuthorizer": "Authorizer1"}, + }, + }, + } + } + to_py27_compatible_template(template) + + _convert_to_py27_type_mock.assert_called_once_with( + {"Stage": "myStage", "Auth": {"Authorizers": {"Authorizer1": {}}, "DefaultAuthorizer": "Authorizer1"}} + ) + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_explit_httpapi_with_global_default_authorizer(self, _convert_to_py27_type_mock): + template = { + "Globals": {"HttpApi": {"Auth": {"Authorizers": {"Authorizer1": {}}, "DefaultAuthorizer": "Authorizer1"}}}, + "Resources": { + "Api": { + "Type": "AWS::Serverless::HttpApi", + "Properties": { + "Stage": "myStage", + }, + }, + }, + } + to_py27_compatible_template(template) + + _convert_to_py27_type_mock.assert_called_once_with( + { + "Stage": "myStage", + } + ) + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_explit_httpapi_with_no_default_authorizer(self, _convert_to_py27_type_mock): + template = { + "Resources": { + "Api": { + "Type": "AWS::Serverless::HttpApi", + "Properties": { + "Stage": "myStage", + }, + }, + } + } + to_py27_compatible_template(template) + + _convert_to_py27_type_mock.assert_not_called() + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_explicit_httpapi_with_httpapi_event(self, _convert_to_py27_type_mock): + template = { + "Resources": { + "Api": { + "Type": "AWS::Serverless::HttpApi", + "Properties": { + "Stage": "myStage", + "Auth": {"Authorizers": {"Authorizer1": {}}, "DefaultAuthorizer": "Authorizer1"}, + }, + }, + "Function": { + "Type": "AWS::Serverless::Function", + "Properties": { + "FunctionName": "MyFunctionName", + "Events": { + "ApiEvent": {"Type": "HttpApi", "Properties": {"Path": "/user", "Method": "GET"}}, + "ApiEvent2": {"Type": "HttpApi", "Properties": {"Path": "/user", "Method": "POST"}}, + }, + }, + }, + } + } + to_py27_compatible_template(template) + + _convert_to_py27_type_mock.assert_any_call( + {"Stage": "myStage", "Auth": {"Authorizers": {"Authorizer1": {}}, "DefaultAuthorizer": "Authorizer1"}} + ) + _convert_to_py27_type_mock.assert_any_call("MyFunctionName") + _convert_to_py27_type_mock.assert_any_call( + { + "ApiEvent": {"Type": "HttpApi", "Properties": {"Path": "/user", "Method": "GET"}}, + "ApiEvent2": {"Type": "HttpApi", "Properties": {"Path": "/user", "Method": "POST"}}, + } + ) + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_implicit_httpapi(self, _convert_to_py27_type_mock): + template = { + "Globals": {"HttpApi": {"Auth": {"Authorizers": {"Authorizer1": {}}, "DefaultAuthorizer": "Authorizer1"}}}, + "Resources": { + "Function1": { + "Type": "AWS::S3::Bucket", + }, + "Function2": { + "Type": "AWS::Serverless::Function", + "Properties": { + "FunctionName": "MyFunctionName2", + }, + }, + "Function3": { + "Type": "AWS::Serverless::Function", + "Properties": { + "FunctionName": "MyFunctionName3", + "Events": { + "ApiEvent3": {"Type": "HttpApi", "Properties": {"Path": "/user", "Method": "GET"}}, + }, + }, + }, + }, + } + to_py27_compatible_template(template) + + _convert_to_py27_type_mock.assert_any_call("MyFunctionName2") + _convert_to_py27_type_mock.assert_any_call("MyFunctionName3") + _convert_to_py27_type_mock.assert_any_call( + { + "ApiEvent3": {"Type": "HttpApi", "Properties": {"Path": "/user", "Method": "GET"}}, + } + ) + + @patch("samtranslator.utils.py27hash_fix._convert_to_py27_type") + def test_implicit_httpapi_with_no_globals(self, _convert_to_py27_type_mock): + template = { + "Resources": { + "Function": { + "Type": "AWS::Serverless::Function", + "Properties": { + "FunctionName": "MyFunctionName", + "Events": { + "ApiEvent": {"Type": "HttpApi", "Properties": {"Path": "/user", "Method": "GET"}}, + }, + }, + }, + } + } + + to_py27_compatible_template(template) + _convert_to_py27_type_mock.assert_not_called() diff --git a/tox.ini b/tox.ini index 63aea07e2..1c3e6577e 100644 --- a/tox.ini +++ b/tox.ini @@ -4,23 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py36, py37, py38 - -[testenv:py27] -# Set this environment variable **only** for Python2.7. In Py >= 3.3, the hash seed property was set to a random -# value on every execution. This is the seed for random number generator used to compute hash of objects. This hash -# is widely used in ``dict`` to compute insertion position. In Py2.7, this seed is uninitialized so when iterating -# over a dictionary, you tend to get the same order always. Unfortunately, this consistency is because the seed remains -# same and not a ordering guarantee provided by the dictionary. -# -# SAM Translator has a dependency on this pseudo-ordering to generate a stable LogicalID for API Gateway Deployment -# resource. Tox tries to simulate Py3 behavior in Py2.7 by setting PYTHONHASHSEED to random values on every run. -# This results in unit test failures. This happens only within Tox. To fix this, we are unsetting the seed value -# specifically for Py27 in Tox. -passenv = AWS* CODECOV_TOKEN -setenv = PYTHONHASHSEED = 0 -commands = make pr2.7 - codecov +envlist = py36, py37, py38 [testenv] commands = make pr From ab6943a340a3f489af62b8c70c1366242b2887fe Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Mon, 3 Jan 2022 17:47:16 -0800 Subject: [PATCH 22/59] chore: bump version to 1.43.0 (#2276) --- samtranslator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index 5d0a428bc..eb012bb42 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.42.0" +__version__ = "1.43.0" From 1fb76866676b56c688f59da701447fea2caabcc0 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Tue, 11 Jan 2022 13:29:23 -0800 Subject: [PATCH 23/59] Update integ test expected result to incorporate py27hashfix changes (#2286) --- .../resources/expected/single/basic_api_with_mode_update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/resources/expected/single/basic_api_with_mode_update.json b/integration/resources/expected/single/basic_api_with_mode_update.json index bf701eca5..a862e3e17 100644 --- a/integration/resources/expected/single/basic_api_with_mode_update.json +++ b/integration/resources/expected/single/basic_api_with_mode_update.json @@ -1,6 +1,6 @@ [ {"LogicalResourceId": "MyApi", "ResourceType": "AWS::ApiGateway::RestApi"}, - {"LogicalResourceId": "MyApiDeploymentada889e3ac", "ResourceType": "AWS::ApiGateway::Deployment"}, + {"LogicalResourceId": "MyApiDeploymentb30ecf9df1", "ResourceType": "AWS::ApiGateway::Deployment"}, {"LogicalResourceId": "MyApiMyNewStageNameStage", "ResourceType": "AWS::ApiGateway::Stage"}, {"LogicalResourceId": "TestFunction", "ResourceType": "AWS::Lambda::Function"}, {"LogicalResourceId": "TestFunctionAliaslive", "ResourceType": "AWS::Lambda::Alias"}, From b3fd85d993e4dada1ce2538ffbf217408915436d Mon Sep 17 00:00:00 2001 From: Phil Corbett <1835431+Phuurl@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:17:46 +0000 Subject: [PATCH 24/59] docs: update examples link (#2277) --- examples/apps/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/apps/README.md b/examples/apps/README.md index 231ecf1c9..6a1b8cfca 100644 --- a/examples/apps/README.md +++ b/examples/apps/README.md @@ -1 +1 @@ -All example applications have been moved to [aws-samples/serverless-app-examples](https://github.com/aws-samples/serverless-app-examples). +All example/template applications have been moved to [aws/aws-sam-cli-app-templates](https://github.com/aws/aws-sam-cli-app-templates). From 85177ef1c7d976b1255cb25971374ceb83199e12 Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Mon, 17 Jan 2022 11:36:36 -0800 Subject: [PATCH 25/59] fix: Raise exception if provided Auth.Authorizers.Identity isn't a dict (#2273) * Raise exception if auth identity isn't a dict * Add tests for congito auth and lambda request auth * Check if property is a dict instead of using try/catch * Fix error message --- samtranslator/model/apigateway.py | 7 +++++++ .../error_api_invalid_auth_identity_cognito.yaml | 11 +++++++++++ ...or_api_invalid_auth_identity_lambda_request.yaml | 13 +++++++++++++ .../error_api_invalid_auth_identity_cognito.json | 8 ++++++++ ...or_api_invalid_auth_identity_lambda_request.json | 8 ++++++++ 5 files changed, 47 insertions(+) create mode 100644 tests/translator/input/error_api_invalid_auth_identity_cognito.yaml create mode 100644 tests/translator/input/error_api_invalid_auth_identity_lambda_request.yaml create mode 100644 tests/translator/output/error_api_invalid_auth_identity_cognito.json create mode 100644 tests/translator/output/error_api_invalid_auth_identity_lambda_request.json diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 41dd19f76..7dc400b92 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -405,6 +405,13 @@ def _get_type(self): return "LAMBDA" def _get_identity_header(self): + if self.identity and not isinstance(self.identity, dict): + raise InvalidResourceException( + self.api_logical_id, + "Auth.Authorizers..Identity must be a dict (LambdaTokenAuthorizationIdentity, " + "LambdaRequestAuthorizationIdentity or CognitoAuthorizationIdentity).", + ) + if not self.identity or not self.identity.get("Header"): return "Authorization" diff --git a/tests/translator/input/error_api_invalid_auth_identity_cognito.yaml b/tests/translator/input/error_api_invalid_auth_identity_cognito.yaml new file mode 100644 index 000000000..fd05ff69c --- /dev/null +++ b/tests/translator/input/error_api_invalid_auth_identity_cognito.yaml @@ -0,0 +1,11 @@ +Resources: + ServerlessApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Auth: + Authorizers: + CognitoAuthorizer: + UserPoolArn: + - Fn::Sub: arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${PoolId} + Identity: CognitoAuthorizationIdentity diff --git a/tests/translator/input/error_api_invalid_auth_identity_lambda_request.yaml b/tests/translator/input/error_api_invalid_auth_identity_lambda_request.yaml new file mode 100644 index 000000000..ec8a45f9b --- /dev/null +++ b/tests/translator/input/error_api_invalid_auth_identity_lambda_request.yaml @@ -0,0 +1,13 @@ +Resources: + ServerlessApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Auth: + Authorizers: + LambdaRequestAuthorizer: + FunctionArn: + Fn::GetAtt: + - MyAuthFunction + - Arn + Identity: LambdaRequestAuthorizationIdentity diff --git a/tests/translator/output/error_api_invalid_auth_identity_cognito.json b/tests/translator/output/error_api_invalid_auth_identity_cognito.json new file mode 100644 index 000000000..87f756a92 --- /dev/null +++ b/tests/translator/output/error_api_invalid_auth_identity_cognito.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [ServerlessApi] is invalid. Auth.Authorizers..Identity must be a dict (LambdaTokenAuthorizationIdentity, LambdaRequestAuthorizationIdentity or CognitoAuthorizationIdentity)." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [ServerlessApi] is invalid. Auth.Authorizers..Identity must be a dict (LambdaTokenAuthorizationIdentity, LambdaRequestAuthorizationIdentity or CognitoAuthorizationIdentity)." +} diff --git a/tests/translator/output/error_api_invalid_auth_identity_lambda_request.json b/tests/translator/output/error_api_invalid_auth_identity_lambda_request.json new file mode 100644 index 000000000..87f756a92 --- /dev/null +++ b/tests/translator/output/error_api_invalid_auth_identity_lambda_request.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [ServerlessApi] is invalid. Auth.Authorizers..Identity must be a dict (LambdaTokenAuthorizationIdentity, LambdaRequestAuthorizationIdentity or CognitoAuthorizationIdentity)." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [ServerlessApi] is invalid. Auth.Authorizers..Identity must be a dict (LambdaTokenAuthorizationIdentity, LambdaRequestAuthorizationIdentity or CognitoAuthorizationIdentity)." +} From a65e59efd201bcf68f663dd0e727905fbfaf6360 Mon Sep 17 00:00:00 2001 From: Joe Lafiosca Date: Mon, 17 Jan 2022 18:30:33 -0500 Subject: [PATCH 26/59] fix: Correct CognitoUserPool SmsConfiguration validation type (#1582) * correct CognitoUserPool SmsConfiguration type from list of dict to dict; fixes #1252 * Add tests for cognito userpool sms configuration type change Co-authored-by: Daniel Mil --- samtranslator/model/cognito.py | 2 +- samtranslator/parser/parser.py | 2 +- ...ngito_userpool_with_sms_configuration.yaml | 23 +++++ ...ngito_userpool_with_sms_configuration.json | 98 +++++++++++++++++++ ...ngito_userpool_with_sms_configuration.json | 98 +++++++++++++++++++ ...ngito_userpool_with_sms_configuration.json | 98 +++++++++++++++++++ tests/translator/test_translator.py | 1 + 7 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 tests/translator/input/congito_userpool_with_sms_configuration.yaml create mode 100644 tests/translator/output/aws-cn/congito_userpool_with_sms_configuration.json create mode 100644 tests/translator/output/aws-us-gov/congito_userpool_with_sms_configuration.json create mode 100644 tests/translator/output/congito_userpool_with_sms_configuration.json diff --git a/samtranslator/model/cognito.py b/samtranslator/model/cognito.py index d44e1cf3d..45f1645d5 100644 --- a/samtranslator/model/cognito.py +++ b/samtranslator/model/cognito.py @@ -20,7 +20,7 @@ class CognitoUserPool(Resource): "Policies": PropertyType(False, is_type(dict)), "Schema": PropertyType(False, list_of(dict)), "SmsAuthenticationMessage": PropertyType(False, is_str()), - "SmsConfiguration": PropertyType(False, list_of(dict)), + "SmsConfiguration": PropertyType(False, is_type(dict)), "SmsVerificationMessage": PropertyType(False, is_str()), "UsernameAttributes": PropertyType(False, list_of(is_str())), "UsernameConfiguration": PropertyType(False, is_type(dict)), diff --git a/samtranslator/parser/parser.py b/samtranslator/parser/parser.py index 9d07e43c2..92e5c9cad 100644 --- a/samtranslator/parser/parser.py +++ b/samtranslator/parser/parser.py @@ -18,7 +18,7 @@ def parse(self, sam_template, parameter_values, sam_plugins): @staticmethod def validate_datatypes(sam_template): - """Validates the datatype within the template """ + """Validates the datatype within the template""" if ( "Resources" not in sam_template or not isinstance(sam_template["Resources"], dict) diff --git a/tests/translator/input/congito_userpool_with_sms_configuration.yaml b/tests/translator/input/congito_userpool_with_sms_configuration.yaml new file mode 100644 index 000000000..4677a7b8a --- /dev/null +++ b/tests/translator/input/congito_userpool_with_sms_configuration.yaml @@ -0,0 +1,23 @@ +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: app.handler + Runtime: nodejs14.x + Events: + CognitoUserPoolPreSignup: + Type: Cognito + Properties: + UserPool: + Ref: MyCognitoUserPool + Trigger: + PreSignUp + + MyCognitoUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: PreSignup + SmsConfiguration: + SnsCallerArn: !GetAtt UserPoolRole.Arn + ExternalId: !Ref ExternalId diff --git a/tests/translator/output/aws-cn/congito_userpool_with_sms_configuration.json b/tests/translator/output/aws-cn/congito_userpool_with_sms_configuration.json new file mode 100644 index 000000000..3c37f2d4f --- /dev/null +++ b/tests/translator/output/aws-cn/congito_userpool_with_sms_configuration.json @@ -0,0 +1,98 @@ +{ + "Resources": { + "MyCognitoUserPool": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "LambdaConfig": { + "PreSignUp": { + "Fn::GetAtt": [ + "HelloWorldFunction", + "Arn" + ] + } + }, + "SmsConfiguration": { + "SnsCallerArn": { + "Fn::GetAtt": [ + "UserPoolRole", + "Arn" + ] + }, + "ExternalId": { + "Ref": "ExternalId" + } + }, + "UserPoolName": "PreSignup" + } + }, + "HelloWorldFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "Handler": "app.handler", + "Role": { + "Fn::GetAtt": [ + "HelloWorldFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HelloWorldFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HelloWorldFunctionCognitoPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HelloWorldFunction" + }, + "Principal": "cognito-idp.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "MyCognitoUserPool", + "Arn" + ] + } + } + } + } +} diff --git a/tests/translator/output/aws-us-gov/congito_userpool_with_sms_configuration.json b/tests/translator/output/aws-us-gov/congito_userpool_with_sms_configuration.json new file mode 100644 index 000000000..d08992be5 --- /dev/null +++ b/tests/translator/output/aws-us-gov/congito_userpool_with_sms_configuration.json @@ -0,0 +1,98 @@ +{ + "Resources": { + "MyCognitoUserPool": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "LambdaConfig": { + "PreSignUp": { + "Fn::GetAtt": [ + "HelloWorldFunction", + "Arn" + ] + } + }, + "SmsConfiguration": { + "SnsCallerArn": { + "Fn::GetAtt": [ + "UserPoolRole", + "Arn" + ] + }, + "ExternalId": { + "Ref": "ExternalId" + } + }, + "UserPoolName": "PreSignup" + } + }, + "HelloWorldFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "Handler": "app.handler", + "Role": { + "Fn::GetAtt": [ + "HelloWorldFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HelloWorldFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HelloWorldFunctionCognitoPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HelloWorldFunction" + }, + "Principal": "cognito-idp.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "MyCognitoUserPool", + "Arn" + ] + } + } + } + } +} diff --git a/tests/translator/output/congito_userpool_with_sms_configuration.json b/tests/translator/output/congito_userpool_with_sms_configuration.json new file mode 100644 index 000000000..3ec561c39 --- /dev/null +++ b/tests/translator/output/congito_userpool_with_sms_configuration.json @@ -0,0 +1,98 @@ +{ + "Resources": { + "MyCognitoUserPool": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "LambdaConfig": { + "PreSignUp": { + "Fn::GetAtt": [ + "HelloWorldFunction", + "Arn" + ] + } + }, + "SmsConfiguration": { + "SnsCallerArn": { + "Fn::GetAtt": [ + "UserPoolRole", + "Arn" + ] + }, + "ExternalId": { + "Ref": "ExternalId" + } + }, + "UserPoolName": "PreSignup" + } + }, + "HelloWorldFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "bucket", + "S3Key": "key" + }, + "Handler": "app.handler", + "Role": { + "Fn::GetAtt": [ + "HelloWorldFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HelloWorldFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "HelloWorldFunctionCognitoPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "HelloWorldFunction" + }, + "Principal": "cognito-idp.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "MyCognitoUserPool", + "Arn" + ] + } + } + } + } +} diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 577ad266e..7f253deb1 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -254,6 +254,7 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): @parameterized.expand( itertools.product( [ + "congito_userpool_with_sms_configuration", "cognito_userpool_with_event", "s3_with_condition", "function_with_condition", From 4b446dd17432ccf833b60bc5d88db911e123f8af Mon Sep 17 00:00:00 2001 From: Mohamed Elasmar <71043312+moelasmar@users.noreply.github.com> Date: Mon, 17 Jan 2022 18:47:32 -0800 Subject: [PATCH 27/59] fix: accept empty components property in Open Api Definition (#2296) * use a default dict in case if a template contains an empty components property in the Open Api definition body instead of failing, as Components property is an optional property in the OpenApi definition. * add some comments to the code, and more test cases --- samtranslator/model/api/api_generator.py | 4 + ...th_security_definition_and_components.yaml | 43 ++++++ ...security_definition_and_no_components.yaml | 36 +++++ ...curity_definition_and_none_components.yaml | 37 +++++ ...th_security_definition_and_components.json | 133 +++++++++++++++++ ...security_definition_and_no_components.json | 123 +++++++++++++++ ...curity_definition_and_none_components.json | 123 +++++++++++++++ ...th_security_definition_and_components.json | 141 ++++++++++++++++++ ...security_definition_and_no_components.json | 131 ++++++++++++++++ ...curity_definition_and_none_components.json | 131 ++++++++++++++++ ...th_security_definition_and_components.json | 141 ++++++++++++++++++ ...security_definition_and_no_components.json | 131 ++++++++++++++++ ...curity_definition_and_none_components.json | 131 ++++++++++++++++ tests/translator/test_translator.py | 3 + 14 files changed, 1308 insertions(+) create mode 100644 tests/translator/input/api_with_security_definition_and_components.yaml create mode 100644 tests/translator/input/api_with_security_definition_and_no_components.yaml create mode 100644 tests/translator/input/api_with_security_definition_and_none_components.yaml create mode 100644 tests/translator/output/api_with_security_definition_and_components.json create mode 100644 tests/translator/output/api_with_security_definition_and_no_components.json create mode 100644 tests/translator/output/api_with_security_definition_and_none_components.json create mode 100644 tests/translator/output/aws-cn/api_with_security_definition_and_components.json create mode 100644 tests/translator/output/aws-cn/api_with_security_definition_and_no_components.json create mode 100644 tests/translator/output/aws-cn/api_with_security_definition_and_none_components.json create mode 100644 tests/translator/output/aws-us-gov/api_with_security_definition_and_components.json create mode 100644 tests/translator/output/aws-us-gov/api_with_security_definition_and_no_components.json create mode 100644 tests/translator/output/aws-us-gov/api_with_security_definition_and_none_components.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index c1a07fcdf..47089a102 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -999,6 +999,10 @@ def _openapi_postprocess(self, definition_body): ): if definition_body.get("securityDefinitions"): components = definition_body.get("components", Py27Dict()) + # In the previous line, the default value `Py27Dict()` will be only returned only if `components` + # property is not in definition_body dict, but if it exist, and its value is None, so None will be + # returned and not the default value. That is why the below line is required. + components = components if components else Py27Dict() components["securitySchemes"] = definition_body["securityDefinitions"] definition_body["components"] = components del definition_body["securityDefinitions"] diff --git a/tests/translator/input/api_with_security_definition_and_components.yaml b/tests/translator/input/api_with_security_definition_and_components.yaml new file mode 100644 index 000000000..143b2bf0c --- /dev/null +++ b/tests/translator/input/api_with_security_definition_and_components.yaml @@ -0,0 +1,43 @@ +Resources: + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs12.x + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: + info: + version: '1.0' + title: + Ref: AWS::StackName + securityDefinitions: # 1 Add security definition + CognitoAuthorizer: + type: "apiKey" + name: "Authorization" + in: "header" + x-amazon-apigateway-authtype: "cognito_user_pools" + x-amazon-apigateway-authorizer: + providerARNs: + - # userPool ARN + type: "cognito_user_pools" + paths: + "/{proxy+}": + x-amazon-apigateway-any-method: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations + responses: { } + components: + schemas: + Error: + type: Object + properties: + message: + type: string + openapi: '3.0.0' diff --git a/tests/translator/input/api_with_security_definition_and_no_components.yaml b/tests/translator/input/api_with_security_definition_and_no_components.yaml new file mode 100644 index 000000000..4f3920614 --- /dev/null +++ b/tests/translator/input/api_with_security_definition_and_no_components.yaml @@ -0,0 +1,36 @@ +Resources: + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs12.x + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: + info: + version: '1.0' + title: + Ref: AWS::StackName + securityDefinitions: # 1 Add security definition + CognitoAuthorizer: + type: "apiKey" + name: "Authorization" + in: "header" + x-amazon-apigateway-authtype: "cognito_user_pools" + x-amazon-apigateway-authorizer: + providerARNs: + - # userPool ARN + type: "cognito_user_pools" + paths: + "/{proxy+}": + x-amazon-apigateway-any-method: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations + responses: { } + openapi: '3.0.0' diff --git a/tests/translator/input/api_with_security_definition_and_none_components.yaml b/tests/translator/input/api_with_security_definition_and_none_components.yaml new file mode 100644 index 000000000..7cad904f7 --- /dev/null +++ b/tests/translator/input/api_with_security_definition_and_none_components.yaml @@ -0,0 +1,37 @@ +Resources: + GetHtmlFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.handler + Runtime: nodejs12.x + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: + info: + version: '1.0' + title: + Ref: AWS::StackName + securityDefinitions: # 1 Add security definition + CognitoAuthorizer: + type: "apiKey" + name: "Authorization" + in: "header" + x-amazon-apigateway-authtype: "cognito_user_pools" + x-amazon-apigateway-authorizer: + providerARNs: + - # userPool ARN + type: "cognito_user_pools" + paths: + "/{proxy+}": + x-amazon-apigateway-any-method: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations + responses: { } + components: + openapi: '3.0.0' diff --git a/tests/translator/output/api_with_security_definition_and_components.json b/tests/translator/output/api_with_security_definition_and_components.json new file mode 100644 index 000000000..d6cf67085 --- /dev/null +++ b/tests/translator/output/api_with_security_definition_and_components.json @@ -0,0 +1,133 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiDeployment195f5bf5d0": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 195f5bf5d07bf7af9c64f0649d2724425b106350", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment195f5bf5d0" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + }, + "schemas": { + "Error": { + "type": "Object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/api_with_security_definition_and_no_components.json b/tests/translator/output/api_with_security_definition_and_no_components.json new file mode 100644 index 000000000..69c13968b --- /dev/null +++ b/tests/translator/output/api_with_security_definition_and_no_components.json @@ -0,0 +1,123 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment407993a935" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeployment407993a935": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 407993a9358b76c8e74599b2c0b914409ee0da64", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/api_with_security_definition_and_none_components.json b/tests/translator/output/api_with_security_definition_and_none_components.json new file mode 100644 index 000000000..236ec8c47 --- /dev/null +++ b/tests/translator/output/api_with_security_definition_and_none_components.json @@ -0,0 +1,123 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "Arn", + "GetHtmlFunctionRole" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment61b3921bb7" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeployment61b3921bb7": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 61b3921bb7522c20a8e0de1d24c974267f3ec17b", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_with_security_definition_and_components.json b/tests/translator/output/aws-cn/api_with_security_definition_and_components.json new file mode 100644 index 000000000..b1f5e6e92 --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_security_definition_and_components.json @@ -0,0 +1,141 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiDeployment195f5bf5d0": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 195f5bf5d07bf7af9c64f0649d2724425b106350", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment195f5bf5d0" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + }, + "schemas": { + "Error": { + "type": "Object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_with_security_definition_and_no_components.json b/tests/translator/output/aws-cn/api_with_security_definition_and_no_components.json new file mode 100644 index 000000000..27bd3ad28 --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_security_definition_and_no_components.json @@ -0,0 +1,131 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment407993a935" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeployment407993a935": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 407993a9358b76c8e74599b2c0b914409ee0da64", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_with_security_definition_and_none_components.json b/tests/translator/output/aws-cn/api_with_security_definition_and_none_components.json new file mode 100644 index 000000000..5f7d1aa07 --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_security_definition_and_none_components.json @@ -0,0 +1,131 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "Arn", + "GetHtmlFunctionRole" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment61b3921bb7" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeployment61b3921bb7": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 61b3921bb7522c20a8e0de1d24c974267f3ec17b", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_with_security_definition_and_components.json b/tests/translator/output/aws-us-gov/api_with_security_definition_and_components.json new file mode 100644 index 000000000..665cedf94 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_security_definition_and_components.json @@ -0,0 +1,141 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiDeployment195f5bf5d0": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 195f5bf5d07bf7af9c64f0649d2724425b106350", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment195f5bf5d0" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + }, + "schemas": { + "Error": { + "type": "Object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_with_security_definition_and_no_components.json b/tests/translator/output/aws-us-gov/api_with_security_definition_and_no_components.json new file mode 100644 index 000000000..fd188a2b3 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_security_definition_and_no_components.json @@ -0,0 +1,131 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment407993a935" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeployment407993a935": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 407993a9358b76c8e74599b2c0b914409ee0da64", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_with_security_definition_and_none_components.json b/tests/translator/output/aws-us-gov/api_with_security_definition_and_none_components.json new file mode 100644 index 000000000..83fb7900c --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_security_definition_and_none_components.json @@ -0,0 +1,131 @@ +{ + "Resources": { + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "Arn", + "GetHtmlFunctionRole" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment61b3921bb7" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeployment61b3921bb7": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 61b3921bb7522c20a8e0de1d24c974267f3ec17b", + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Stage" + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "openapi": "3.0.0", + "components": { + "securitySchemes": { + "CognitoAuthorizer": { + "x-amazon-apigateway-authtype": "cognito_user_pools", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + null + ], + "type": "cognito_user_pools" + }, + "in": "header" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 7f253deb1..44d7b2274 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -458,6 +458,9 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): "function_with_auth_mechanism_for_self_managed_kafka", "function_with_vpc_permission_for_self_managed_kafka", "function_with_event_filtering", + "api_with_security_definition_and_components", + "api_with_security_definition_and_none_components", + "api_with_security_definition_and_no_components", ], [ ("aws", "ap-southeast-1"), From 6c5981b37448112e1ffd87bac956e8d2fcb6153f Mon Sep 17 00:00:00 2001 From: JiteshKanojia <69892060+JiteshKanojia@users.noreply.github.com> Date: Thu, 20 Jan 2022 23:53:07 +0530 Subject: [PATCH 28/59] Corrected gitpod link to open this repo instead of aws-sam-cli (#2170) --- DEVELOPMENT_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md index 7c6a4356c..76486e538 100644 --- a/DEVELOPMENT_GUIDE.md +++ b/DEVELOPMENT_GUIDE.md @@ -15,7 +15,7 @@ Windows users, consider using [pipenv](https://docs.pipenv.org/). ------------------------- For setting up a local development environment, we recommend using Gitpod - a service that allows you to spin up an in-browser Visual Studio Code-compatible editor, with everything set up and ready to go for development on this project. Just click the button below to create your private workspace: -[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/awslabs/aws-sam-cli) +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/aws/serverless-application-model.git) This will start a new Gitpod workspace, and immediately kick off a build of the code. Once it's done, you can start working. From 24df81bcce94e2883749f8caca0e201156ec6533 Mon Sep 17 00:00:00 2001 From: Wesley Dias Date: Thu, 20 Jan 2022 16:41:29 -0300 Subject: [PATCH 29/59] docs: updating EventBridgeRule documentation (#2236) * adding missing properties of EventBridgeRule * adding DeadLetterConfig Object and RetryPolicy Object to Data Types section * improving EventBridgeRule example --- versions/2016-10-31.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/versions/2016-10-31.md b/versions/2016-10-31.md index 4be371949..e5fbc3d44 100644 --- a/versions/2016-10-31.md +++ b/versions/2016-10-31.md @@ -775,10 +775,13 @@ The object describing an event source with type `EventBridgeRule`. Property Name | Type | Description ---|:---:|--- +DeadLetterConfig | [DeadLetterConfig Object](#deadletterconfig-object) | Configure the Amazon Simple Queue Service (Amazon SQS) queue where EventBridge sends events after a failed target invocation. Pattern | [Event Pattern Object](https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-and-event-patterns.html) | **Required.** Pattern describing which EventBridge events trigger the function. Only matching events trigger the function. EventBusName | `string` | The event bus to associate with this rule. If you omit this, the default event bus is used. Input | `string` | JSON-formatted string to pass to the function as the event body. This value overrides the matched event. InputPath | `string` | JSONPath describing the part of the event to pass to the function. +RetryPolicy | [RetryPolicy Object](#retrypolicy-object) | A RetryPolicy object that includes information about the retry policy settings. +Target | [Target Object](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-target.html) | Configures the AWS resource that EventBridge invokes when a rule is triggered. ##### Example: EventBridge event source object @@ -789,6 +792,14 @@ Properties: detail: state: - terminated + RetryPolicy: + MaximumRetryAttempts: 5 + MaximumEventAgeInSeconds: 900 + DeadLetterConfig: + Type: SQS + QueueLogicalId: EBRuleDLQ + Target: + Id: MyTarget ``` #### CloudWatchLogs @@ -992,6 +1003,8 @@ Properties: - [S3 Location Object](#s3-location-object) - [Application Location Object](#application-id-object) - [DeadLetterQueue Object](#deadletterqueue-object) +- [DeadLetterConfig Object](#deadletterconfig-object) +- [RetryPolicy Object](#retrypolicy-object) - [Cors Configuration](#cors-configuration) - [API EndpointConfiguration Object](#api-endpointconfiguration-object) - [API Auth Object](#api-auth-object) @@ -1037,6 +1050,30 @@ DeadLetterQueue: TargetArn: ARN of the SQS queue or SNS topic to use as DLQ. ``` +#### DeadLetterConfig Object +The object used to specify the Amazon Simple Queue Service (Amazon SQS) queue where EventBridge sends events after a failed target invocation. Invocation can fail, for example, when sending an event to a Lambda function that doesn’t exist, or insufficient permissions to invoke the Lambda function. For more information, see [Event retry policy and using dead-letter queues](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html) in the *Amazon EventBridge User Guide*. + +**Note:** The [AWS::Serverless::Function](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html) resource type has a similar data type, `DeadLetterQueue` which handles failures that occur after successful invocation of the target Lambda function. Examples of this type of failure include Lambda throttling, or errors returned by the Lambda target function. For more information about the function `DeadLetterQueue` property, see [AWS Lambda function dead letter queues](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#invocation-dlq) in the AWS Lambda Developer Guide. + +Syntax: + +```yaml +DeadLetterConfig: + Arn: The Amazon Resource Name (ARN) of the Amazon SQS queue specified as the target for the dead-letter queue. + QueueLogicalId: The custom name of the dead letter queue that AWS SAM creates if `Type` is specified. + Type: `SQS` +``` + +#### RetryPolicy Object +A RetryPolicy object that includes information about the retry policy settings. + +Syntax: + +```yaml +MaximumEventAgeInSeconds: The maximum amount of time, in seconds, to continue to make retry attempts. +MaximumRetryAttempts: The maximum number of retry attempts to make before the request fails. Retry attempts continue until either the maximum number of attempts is made or until the duration of the MaximumEventAgeInSeconds is met. +``` + #### DeploymentPreference Object Specifies the configurations to enable Safe Lambda Deployments. Read the [usage guide](../docs/safe_lambda_deployments.rst) for detailed information. The following shows all available properties of this object. TriggerConfigurations takes a list of [TriggerConfig](https://docs.aws.amazon.com/codedeploy/latest/APIReference/API_TriggerConfig.html) objects. From 93642295ae2ed7b8eb7744a05f36cd9408d15826 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Thu, 20 Jan 2022 14:43:44 -0800 Subject: [PATCH 30/59] Update INTEGRATION_TESTS.md (#2300) --- INTEGRATION_TESTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INTEGRATION_TESTS.md b/INTEGRATION_TESTS.md index 7e92667a1..6a5f4e487 100644 --- a/INTEGRATION_TESTS.md +++ b/INTEGRATION_TESTS.md @@ -2,7 +2,7 @@ These tests run SAM against AWS services by translating SAM templates, deploying them to Cloud Formation and verifying the resulting objects. -They must run successfully under Python 2 and 3. +They must run successfully under Python 3. ## Run the tests From a308593bb8cefd3ddc644f928032fb5507635647 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Thu, 20 Jan 2022 14:44:01 -0800 Subject: [PATCH 31/59] Clean up Py2 dependencies and update to use native unittest.mock (#2299) --- requirements/base.txt | 2 -- requirements/dev.txt | 11 ++++------- tests/feature_toggle/test_feature_toggle.py | 2 +- tests/intrinsics/test_actions.py | 2 +- tests/intrinsics/test_resolver.py | 2 +- tests/metrics/test_method_decorator.py | 2 +- tests/metrics/test_metrics.py | 2 +- tests/model/api/test_api_generator.py | 2 +- tests/model/api/test_http_api_generator.py | 2 +- tests/model/eventsources/test_api_event_source.py | 2 +- .../eventsources/test_cloudwatch_event_source.py | 2 +- .../eventsources/test_cloudwatchlogs_event_source.py | 2 +- .../eventsources/test_eventbridge_rule_source.py | 2 +- .../model/eventsources/test_schedule_event_source.py | 2 +- tests/model/eventsources/test_sns_event_source.py | 2 +- tests/model/stepfunctions/test_api_event.py | 2 +- .../stepfunctions/test_cloudwatchevents_event.py | 2 +- .../stepfunctions/test_eventbridge_rule_source.py | 2 +- tests/model/stepfunctions/test_schedule_event.py | 2 +- .../stepfunctions/test_state_machine_generator.py | 2 +- tests/model/test_api_v2.py | 2 +- tests/model/test_function_policies.py | 2 +- tests/model/test_resource_policies.py | 2 +- tests/model/test_sam_resources.py | 2 +- tests/parser/test_parser.py | 2 +- .../api/test_default_definition_body_plugin.py | 2 +- tests/plugins/api/test_implicit_api_plugin.py | 2 +- .../plugins/application/test_serverless_app_plugin.py | 2 +- tests/plugins/globals/test_globals.py | 2 +- tests/plugins/globals/test_globals_plugin.py | 2 +- .../plugins/policies/test_policy_templates_plugin.py | 2 +- tests/policy_template_processor/test_processor.py | 2 +- tests/policy_template_processor/test_template.py | 2 +- tests/sdk/test_parameter.py | 2 +- tests/swagger/test_swagger.py | 2 +- tests/test_model.py | 2 +- tests/test_plugins.py | 2 +- .../test_deployment_preference_collection.py | 2 +- tests/translator/test_api_resource.py | 2 +- tests/translator/test_arn_generator.py | 2 +- tests/translator/test_function_resources.py | 2 +- tests/translator/test_logical_id_generator.py | 2 +- tests/translator/test_managed_policies_translator.py | 2 +- tests/translator/test_resource_level_attributes.py | 2 +- tests/translator/test_translator.py | 2 +- .../test_deployment_preference_collection.py | 2 +- tests/unit/test_region_configuration.py | 2 +- tests/unit/translator/test_arn_generator.py | 2 +- tests/utils/test_py27hash_fix.py | 2 +- 49 files changed, 51 insertions(+), 56 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index d1c65e5cf..e8c9701a2 100755 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,6 +1,4 @@ -pyrsistent~=0.16.0; python_version<"3" boto3~=1.5 -enum34~=1.1; python_version<"3.4" jsonschema~=3.2 six~=1.15 diff --git a/requirements/dev.txt b/requirements/dev.txt index ee58bcfa2..eebaee996 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,19 +1,16 @@ coverage~=5.3 flake8~=3.8.4 -tox~=3.20.1 +tox~=3.24 pytest-cov~=2.10.1 -pytest-xdist~=1.34.0 # pytest-xdist 2 is not compatible with Python 2.7 +pytest-xdist~=2.5 pylint>=1.7.2,<2.0 pyyaml~=5.4 # Test requirements -pytest~=6.1.1; python_version >= '3.6' -pytest~=4.6.11; python_version < '3.6' # pytest dropped python 2 support after 4.6.x -mock>=3.0.5,<4.0.0 # 4.0.0 drops Python 2 support +pytest~=6.2.5 parameterized~=0.7.4 # Integration tests -pathlib2>=2.3.5; python_version < '3' click~=7.1 dateparser~=0.7 boto3~=1.17 @@ -25,4 +22,4 @@ requests~=2.24.0 docopt~=0.6.2 # formatter -black==20.8b1; python_version >= '3.6' +black==20.8b1 diff --git a/tests/feature_toggle/test_feature_toggle.py b/tests/feature_toggle/test_feature_toggle.py index 56a432863..a8788a4e2 100644 --- a/tests/feature_toggle/test_feature_toggle.py +++ b/tests/feature_toggle/test_feature_toggle.py @@ -1,4 +1,4 @@ -from mock import patch, Mock +from unittest.mock import patch, Mock from parameterized import parameterized, param from unittest import TestCase import os, sys diff --git a/tests/intrinsics/test_actions.py b/tests/intrinsics/test_actions.py index 4152fa3b7..abe4afa61 100644 --- a/tests/intrinsics/test_actions.py +++ b/tests/intrinsics/test_actions.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import patch, Mock +from unittest.mock import patch, Mock from samtranslator.intrinsics.actions import Action, RefAction, SubAction, GetAttAction, FindInMapAction from samtranslator.intrinsics.resource_refs import SupportedResourceReferences from samtranslator.model.exceptions import InvalidTemplateException, InvalidDocumentException diff --git a/tests/intrinsics/test_resolver.py b/tests/intrinsics/test_resolver.py index 946307b87..e06186051 100644 --- a/tests/intrinsics/test_resolver.py +++ b/tests/intrinsics/test_resolver.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import Mock, patch +from unittest.mock import Mock, patch from samtranslator.intrinsics.resolver import IntrinsicsResolver from samtranslator.intrinsics.actions import Action from samtranslator.model.exceptions import InvalidDocumentException diff --git a/tests/metrics/test_method_decorator.py b/tests/metrics/test_method_decorator.py index 05fbd16ff..f864eeeae 100644 --- a/tests/metrics/test_method_decorator.py +++ b/tests/metrics/test_method_decorator.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import Mock, patch, ANY +from unittest.mock import Mock, patch, ANY from samtranslator.metrics.method_decorator import ( MetricsMethodWrapperSingleton, diff --git a/tests/metrics/test_metrics.py b/tests/metrics/test_metrics.py index aafa151fc..55cc29123 100644 --- a/tests/metrics/test_metrics.py +++ b/tests/metrics/test_metrics.py @@ -1,6 +1,6 @@ from parameterized import parameterized, param from unittest import TestCase -from mock import MagicMock, call, ANY +from unittest.mock import MagicMock, call, ANY from samtranslator.metrics.metrics import ( Metrics, MetricsPublisher, diff --git a/tests/model/api/test_api_generator.py b/tests/model/api/test_api_generator.py index ca0573f04..391b3606f 100644 --- a/tests/model/api/test_api_generator.py +++ b/tests/model/api/test_api_generator.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import Mock, patch +from unittest.mock import Mock, patch from parameterized import parameterized diff --git a/tests/model/api/test_http_api_generator.py b/tests/model/api/test_http_api_generator.py index e5672933f..03030b1f7 100644 --- a/tests/model/api/test_http_api_generator.py +++ b/tests/model/api/test_http_api_generator.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import patch +from unittest.mock import patch import pytest from functools import reduce diff --git a/tests/model/eventsources/test_api_event_source.py b/tests/model/eventsources/test_api_event_source.py index 6357e1d69..bb8bf11e8 100644 --- a/tests/model/eventsources/test_api_event_source.py +++ b/tests/model/eventsources/test_api_event_source.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from samtranslator.model.eventsources.push import Api diff --git a/tests/model/eventsources/test_cloudwatch_event_source.py b/tests/model/eventsources/test_cloudwatch_event_source.py index b323263aa..510f02571 100644 --- a/tests/model/eventsources/test_cloudwatch_event_source.py +++ b/tests/model/eventsources/test_cloudwatch_event_source.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from samtranslator.model.eventsources.push import CloudWatchEvent diff --git a/tests/model/eventsources/test_cloudwatchlogs_event_source.py b/tests/model/eventsources/test_cloudwatchlogs_event_source.py index 4f8f8b707..4d3b71348 100644 --- a/tests/model/eventsources/test_cloudwatchlogs_event_source.py +++ b/tests/model/eventsources/test_cloudwatchlogs_event_source.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from samtranslator.model.eventsources.cloudwatchlogs import CloudWatchLogs diff --git a/tests/model/eventsources/test_eventbridge_rule_source.py b/tests/model/eventsources/test_eventbridge_rule_source.py index e1d031d4c..b4a71bcce 100644 --- a/tests/model/eventsources/test_eventbridge_rule_source.py +++ b/tests/model/eventsources/test_eventbridge_rule_source.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from samtranslator.model.eventsources.push import EventBridgeRule diff --git a/tests/model/eventsources/test_schedule_event_source.py b/tests/model/eventsources/test_schedule_event_source.py index 3bf1b373d..2dff522ca 100644 --- a/tests/model/eventsources/test_schedule_event_source.py +++ b/tests/model/eventsources/test_schedule_event_source.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from samtranslator.model.eventsources.push import Schedule diff --git a/tests/model/eventsources/test_sns_event_source.py b/tests/model/eventsources/test_sns_event_source.py index 711f2ce22..1bd72516f 100644 --- a/tests/model/eventsources/test_sns_event_source.py +++ b/tests/model/eventsources/test_sns_event_source.py @@ -1,4 +1,4 @@ -from mock import Mock +from unittest.mock import Mock from unittest import TestCase from samtranslator.model.eventsources.push import SNS diff --git a/tests/model/stepfunctions/test_api_event.py b/tests/model/stepfunctions/test_api_event.py index cc0457dac..add6b3600 100644 --- a/tests/model/stepfunctions/test_api_event.py +++ b/tests/model/stepfunctions/test_api_event.py @@ -1,4 +1,4 @@ -from mock import Mock +from unittest.mock import Mock from unittest import TestCase from samtranslator.model.stepfunctions.events import Api diff --git a/tests/model/stepfunctions/test_cloudwatchevents_event.py b/tests/model/stepfunctions/test_cloudwatchevents_event.py index 33c74928e..c0957a7d2 100644 --- a/tests/model/stepfunctions/test_cloudwatchevents_event.py +++ b/tests/model/stepfunctions/test_cloudwatchevents_event.py @@ -1,4 +1,4 @@ -from mock import Mock +from unittest.mock import Mock from unittest import TestCase from samtranslator.model.stepfunctions.events import CloudWatchEvent from samtranslator.model.exceptions import InvalidEventException diff --git a/tests/model/stepfunctions/test_eventbridge_rule_source.py b/tests/model/stepfunctions/test_eventbridge_rule_source.py index 254a91fcc..8e1ef412e 100644 --- a/tests/model/stepfunctions/test_eventbridge_rule_source.py +++ b/tests/model/stepfunctions/test_eventbridge_rule_source.py @@ -1,4 +1,4 @@ -from mock import Mock +from unittest.mock import Mock from unittest import TestCase from samtranslator.model.exceptions import InvalidEventException diff --git a/tests/model/stepfunctions/test_schedule_event.py b/tests/model/stepfunctions/test_schedule_event.py index 423defd23..234cd3879 100644 --- a/tests/model/stepfunctions/test_schedule_event.py +++ b/tests/model/stepfunctions/test_schedule_event.py @@ -1,4 +1,4 @@ -from mock import Mock +from unittest.mock import Mock from unittest import TestCase from samtranslator.model.stepfunctions.events import Schedule from samtranslator.model.exceptions import InvalidEventException diff --git a/tests/model/stepfunctions/test_state_machine_generator.py b/tests/model/stepfunctions/test_state_machine_generator.py index 39f688367..cf5f747e6 100644 --- a/tests/model/stepfunctions/test_state_machine_generator.py +++ b/tests/model/stepfunctions/test_state_machine_generator.py @@ -1,4 +1,4 @@ -from mock import Mock +from unittest.mock import Mock from unittest import TestCase from samtranslator.model import ResourceTypeResolver diff --git a/tests/model/test_api_v2.py b/tests/model/test_api_v2.py index 7299f6ba0..1d5428b8e 100644 --- a/tests/model/test_api_v2.py +++ b/tests/model/test_api_v2.py @@ -1,6 +1,6 @@ from unittest import TestCase +from unittest import mock import pytest -import mock from samtranslator.model import InvalidResourceException from samtranslator.model.apigatewayv2 import ApiGatewayV2Authorizer diff --git a/tests/model/test_function_policies.py b/tests/model/test_function_policies.py index bcbe7f152..d2405284c 100644 --- a/tests/model/test_function_policies.py +++ b/tests/model/test_function_policies.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from samtranslator.model.function_policies import FunctionPolicies, PolicyTypes, PolicyEntry diff --git a/tests/model/test_resource_policies.py b/tests/model/test_resource_policies.py index 8c2731c2c..ff3bfb163 100644 --- a/tests/model/test_resource_policies.py +++ b/tests/model/test_resource_policies.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from samtranslator.model.resource_policies import ResourcePolicies, PolicyTypes, PolicyEntry diff --git a/tests/model/test_sam_resources.py b/tests/model/test_sam_resources.py index 2b2fd808c..b2c639aff 100644 --- a/tests/model/test_sam_resources.py +++ b/tests/model/test_sam_resources.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import patch +from unittest.mock import patch import pytest from samtranslator.intrinsics.resolver import IntrinsicsResolver diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index 43e67a1de..49fa6b0eb 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import patch, Mock, call +from unittest.mock import patch, Mock, call from samtranslator.parser.parser import Parser from samtranslator.plugins import LifeCycleEvents diff --git a/tests/plugins/api/test_default_definition_body_plugin.py b/tests/plugins/api/test_default_definition_body_plugin.py index ad6e1648c..76ab86830 100644 --- a/tests/plugins/api/test_default_definition_body_plugin.py +++ b/tests/plugins/api/test_default_definition_body_plugin.py @@ -1,4 +1,4 @@ -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from samtranslator.plugins.api.default_definition_body_plugin import DefaultDefinitionBodyPlugin diff --git a/tests/plugins/api/test_implicit_api_plugin.py b/tests/plugins/api/test_implicit_api_plugin.py index 1ad927212..e3e27fabd 100644 --- a/tests/plugins/api/test_implicit_api_plugin.py +++ b/tests/plugins/api/test_implicit_api_plugin.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import Mock, patch, call +from unittest.mock import Mock, patch, call from samtranslator.public.sdk.resource import SamResource, SamResourceType from samtranslator.public.exceptions import InvalidEventException, InvalidResourceException, InvalidDocumentException diff --git a/tests/plugins/application/test_serverless_app_plugin.py b/tests/plugins/application/test_serverless_app_plugin.py index c130bb76b..5b56846db 100644 --- a/tests/plugins/application/test_serverless_app_plugin.py +++ b/tests/plugins/application/test_serverless_app_plugin.py @@ -2,7 +2,7 @@ import itertools from botocore.exceptions import ClientError -from mock import Mock, patch +from unittest.mock import Mock, patch from unittest import TestCase from parameterized import parameterized, param diff --git a/tests/plugins/globals/test_globals.py b/tests/plugins/globals/test_globals.py index eef6123ec..d020f26b0 100644 --- a/tests/plugins/globals/test_globals.py +++ b/tests/plugins/globals/test_globals.py @@ -1,7 +1,7 @@ from parameterized import parameterized from unittest import TestCase -from mock import patch, Mock +from unittest.mock import patch, Mock from samtranslator.plugins.globals.globals import GlobalProperties, Globals, InvalidGlobalsSectionException diff --git a/tests/plugins/globals/test_globals_plugin.py b/tests/plugins/globals/test_globals_plugin.py index 925211d01..55e772332 100644 --- a/tests/plugins/globals/test_globals_plugin.py +++ b/tests/plugins/globals/test_globals_plugin.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import patch +from unittest.mock import patch from samtranslator.public.exceptions import InvalidDocumentException from samtranslator.public.plugins import BasePlugin diff --git a/tests/plugins/policies/test_policy_templates_plugin.py b/tests/plugins/policies/test_policy_templates_plugin.py index ce16bc92a..373d7ea6a 100644 --- a/tests/plugins/policies/test_policy_templates_plugin.py +++ b/tests/plugins/policies/test_policy_templates_plugin.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import Mock, MagicMock, patch, call +from unittest.mock import Mock, MagicMock, patch, call from samtranslator.plugins import BasePlugin from samtranslator.model.resource_policies import PolicyTypes, PolicyEntry diff --git a/tests/policy_template_processor/test_processor.py b/tests/policy_template_processor/test_processor.py index 9319080f4..ed1441480 100644 --- a/tests/policy_template_processor/test_processor.py +++ b/tests/policy_template_processor/test_processor.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import mock_open, Mock, patch +from unittest.mock import mock_open, Mock, patch import jsonschema import json diff --git a/tests/policy_template_processor/test_template.py b/tests/policy_template_processor/test_template.py index 9753b3539..03b5858cc 100644 --- a/tests/policy_template_processor/test_template.py +++ b/tests/policy_template_processor/test_template.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import Mock, patch, ANY +from unittest.mock import Mock, patch, ANY from samtranslator.policy_template_processor.template import Template from samtranslator.policy_template_processor.exceptions import InvalidParameterValues, InsufficientParameterValues diff --git a/tests/sdk/test_parameter.py b/tests/sdk/test_parameter.py index afcc3ccc1..d8f24f3af 100644 --- a/tests/sdk/test_parameter.py +++ b/tests/sdk/test_parameter.py @@ -2,7 +2,7 @@ from unittest import TestCase from samtranslator.sdk.parameter import SamParameterValues -from mock import patch, Mock +from unittest.mock import patch, Mock from samtranslator.translator.arn_generator import NoRegionFound diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index 0a50d6a7b..346754369 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -1,7 +1,7 @@ import copy from unittest import TestCase -from mock import Mock +from unittest.mock import Mock from parameterized import parameterized, param from samtranslator.swagger.swagger import SwaggerEditor diff --git a/tests/test_model.py b/tests/test_model.py index 9783be7b5..4ec844032 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1,7 +1,7 @@ import pytest from unittest import TestCase -from mock import Mock, call, ANY +from unittest.mock import Mock, call, ANY from samtranslator.model.exceptions import InvalidResourceException from samtranslator.model import PropertyType, Resource, SamResourceMacro, ResourceTypeResolver from samtranslator.intrinsics.resource_refs import SupportedResourceReferences diff --git a/tests/test_plugins.py b/tests/test_plugins.py index a1bfb97ab..79f7080d9 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -2,7 +2,7 @@ from samtranslator.plugins import SamPlugins, BasePlugin, LifeCycleEvents from unittest import TestCase -from mock import Mock, patch, call +from unittest.mock import Mock, patch, call class TestSamPluginsRegistration(TestCase): diff --git a/tests/translator/model/preferences/test_deployment_preference_collection.py b/tests/translator/model/preferences/test_deployment_preference_collection.py index 1d9b14b2d..6ca0d2c59 100644 --- a/tests/translator/model/preferences/test_deployment_preference_collection.py +++ b/tests/translator/model/preferences/test_deployment_preference_collection.py @@ -1,4 +1,4 @@ -from mock import patch +from unittest.mock import patch from unittest import TestCase from samtranslator.model.codedeploy import CodeDeployApplication diff --git a/tests/translator/test_api_resource.py b/tests/translator/test_api_resource.py index 41be5fddb..ca00bf376 100644 --- a/tests/translator/test_api_resource.py +++ b/tests/translator/test_api_resource.py @@ -2,7 +2,7 @@ import os from unittest import TestCase -from mock import MagicMock, patch +from unittest.mock import MagicMock, patch from tests.translator.helpers import get_template_parameter_values from samtranslator.translator.transform import transform from samtranslator.model.apigateway import ApiGatewayDeployment diff --git a/tests/translator/test_arn_generator.py b/tests/translator/test_arn_generator.py index 200f90414..7a0bfc65c 100644 --- a/tests/translator/test_arn_generator.py +++ b/tests/translator/test_arn_generator.py @@ -1,6 +1,6 @@ from unittest import TestCase from parameterized import parameterized -from mock import patch +from unittest.mock import patch from samtranslator.translator.arn_generator import ArnGenerator, NoRegionFound diff --git a/tests/translator/test_function_resources.py b/tests/translator/test_function_resources.py index aa2d57f4e..279668155 100644 --- a/tests/translator/test_function_resources.py +++ b/tests/translator/test_function_resources.py @@ -1,5 +1,5 @@ from unittest import TestCase -from mock import patch, Mock +from unittest.mock import patch, Mock import os from samtranslator.model.sam_resources import SamFunction from samtranslator.model.lambda_ import LambdaAlias, LambdaVersion, LambdaFunction diff --git a/tests/translator/test_logical_id_generator.py b/tests/translator/test_logical_id_generator.py index 65607defb..06634495a 100644 --- a/tests/translator/test_logical_id_generator.py +++ b/tests/translator/test_logical_id_generator.py @@ -2,7 +2,7 @@ import json from unittest import TestCase -from mock import patch +from unittest.mock import patch from samtranslator.translator.logical_id_generator import LogicalIdGenerator diff --git a/tests/translator/test_managed_policies_translator.py b/tests/translator/test_managed_policies_translator.py index 195db3a38..811f2533f 100644 --- a/tests/translator/test_managed_policies_translator.py +++ b/tests/translator/test_managed_policies_translator.py @@ -1,4 +1,4 @@ -from mock import MagicMock +from unittest.mock import MagicMock from samtranslator.translator.managed_policy_translator import ManagedPolicyLoader diff --git a/tests/translator/test_resource_level_attributes.py b/tests/translator/test_resource_level_attributes.py index 58754fe92..0f8df442d 100644 --- a/tests/translator/test_resource_level_attributes.py +++ b/tests/translator/test_resource_level_attributes.py @@ -1,5 +1,5 @@ import itertools -from mock import patch +from unittest.mock import patch from parameterized import parameterized diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 44d7b2274..4ea9aaa51 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -22,7 +22,7 @@ import yaml from unittest import TestCase from samtranslator.translator.transform import transform -from mock import Mock, MagicMock, patch +from unittest.mock import Mock, MagicMock, patch BASE_PATH = os.path.dirname(__file__) INPUT_FOLDER = BASE_PATH + "/input" diff --git a/tests/unit/model/preferences/test_deployment_preference_collection.py b/tests/unit/model/preferences/test_deployment_preference_collection.py index 3412b9f5c..0ab04c949 100644 --- a/tests/unit/model/preferences/test_deployment_preference_collection.py +++ b/tests/unit/model/preferences/test_deployment_preference_collection.py @@ -1,6 +1,6 @@ from unittest import TestCase -from mock import patch +from unittest.mock import patch from parameterized import parameterized from samtranslator.model.preferences.deployment_preference_collection import DeploymentPreferenceCollection diff --git a/tests/unit/test_region_configuration.py b/tests/unit/test_region_configuration.py index 646b0c136..ac45832f4 100644 --- a/tests/unit/test_region_configuration.py +++ b/tests/unit/test_region_configuration.py @@ -1,6 +1,6 @@ from unittest import TestCase -from mock import patch +from unittest.mock import patch from parameterized import parameterized from samtranslator.region_configuration import RegionConfiguration diff --git a/tests/unit/translator/test_arn_generator.py b/tests/unit/translator/test_arn_generator.py index 3d0d33016..65a6d201b 100644 --- a/tests/unit/translator/test_arn_generator.py +++ b/tests/unit/translator/test_arn_generator.py @@ -1,6 +1,6 @@ from unittest import TestCase -from mock import patch +from unittest.mock import patch from parameterized import parameterized from samtranslator.translator.arn_generator import ArnGenerator diff --git a/tests/utils/test_py27hash_fix.py b/tests/utils/test_py27hash_fix.py index 8d59aa940..55c6b0d56 100644 --- a/tests/utils/test_py27hash_fix.py +++ b/tests/utils/test_py27hash_fix.py @@ -1,7 +1,7 @@ import copy from unittest import TestCase -from mock import patch +from unittest.mock import patch from samtranslator.utils.py27hash_fix import ( Py27Dict, Py27Keys, From ee31707c5faf0db67bd05fbb9a8b8220379655aa Mon Sep 17 00:00:00 2001 From: Tyler Southwick Date: Thu, 20 Jan 2022 14:53:44 -0800 Subject: [PATCH 32/59] add undocumented SecurityPolicy to ApiGateway domain configuration (#1937) --- versions/2016-10-31.md | 1 + 1 file changed, 1 insertion(+) diff --git a/versions/2016-10-31.md b/versions/2016-10-31.md index e5fbc3d44..9e45f1965 100644 --- a/versions/2016-10-31.md +++ b/versions/2016-10-31.md @@ -1331,6 +1331,7 @@ Domain: DomainName: String # REQUIRED | custom domain name being configured on the api, "www.example.com" CertificateArn: String # REQUIRED | Must be a valid [certificate ARN](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-certificatemanager-certificate.html), and for EDGE endpoint configuration the certificate must be in us-east-1 EndpointConfiguration: "EDGE" # optional | Default value is REGIONAL | Accepted values are EDGE | REGIONAL + SecurityPolicy: "TLS_1_2" # optional | Default value is TLS_1_0 | Accepted values are TLS_1_0| TLS_1_2 BasePath: - String # optional | Default value is '/' | List of basepaths to be configured with the ApiGateway Domain Name Route53: # optional | Default behavior is to treat as None - does not create Route53 resources | Enable these settings to create Route53 Recordsets From aec6e180617493d9bd83966b272b9ea199ad8113 Mon Sep 17 00:00:00 2001 From: Qingchuan Ma <69653965+qingchm@users.noreply.github.com> Date: Thu, 20 Jan 2022 14:56:36 -0800 Subject: [PATCH 33/59] fix: Add handling for OpenApi definition that has tags with invalid format (#2295) * Add handling for open api definition that has tags with wrong format * Improvement on error message to print a clearer data type --- samtranslator/open_api/open_api.py | 11 +++ .../error_http_api_invalid_tags_format.yaml | 71 +++++++++++++++++++ .../error_http_api_invalid_tags_format.json | 9 +++ 3 files changed, 91 insertions(+) create mode 100644 tests/translator/input/error_http_api_invalid_tags_format.yaml create mode 100644 tests/translator/output/error_http_api_invalid_tags_format.json diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index 298ec7ead..18d5d1267 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -450,6 +450,17 @@ def add_tags(self, tags): :param dict tags: dictionary of tagName:tagValue pairs. """ for name, value in tags.items(): + # verify the tags definition is in the right format + if not isinstance(self.tags, list): + raise InvalidDocumentException( + [ + InvalidTemplateException( + "Tags in OpenApi DefinitionBody needs to be a list. {} is a {} not a list.".format( + self.tags, type(self.tags).__name__ + ) + ) + ] + ) # find an existing tag with this name if it exists existing_tag = next((existing_tag for existing_tag in self.tags if existing_tag.get("name") == name), None) if existing_tag: diff --git a/tests/translator/input/error_http_api_invalid_tags_format.yaml b/tests/translator/input/error_http_api_invalid_tags_format.yaml new file mode 100644 index 000000000..ae7bf0595 --- /dev/null +++ b/tests/translator/input/error_http_api_invalid_tags_format.yaml @@ -0,0 +1,71 @@ +Conditions: + condition: + Fn::Equals: + - true + - true +Resources: + HttpApiFunction: + Condition: condition + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.restapi + Runtime: python3.7 + Events: + SimpleCase: + Type: HttpApi + Properties: + ApiId: !Ref MyApi + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + DefinitionBody: + info: + version: '1.0' + title: + Ref: AWS::StackName + paths: + "/basic": + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DifferentFunction.Arn}/invocations + payloadFormatVersion: '1.0' + security: + - OpenIdAuth: + - scope3 + responses: {} + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DifferentFunction.Arn}/invocations + payloadFormatVersion: '1.0' + responses: {} + openapi: 3.0.1 + tags: + name: tag1 + components: + securitySchemes: + oauth2Auth: + type: oauth2 + x-amazon-apigateway-authorizer: + identitySource: "$request.querystring.param" + type: jwt + jwtConfiguration: + audience: + - MyApi + issuer: https://www.example.com/v1/connect/oidc + OpenIdAuth: + type: openIdConnect + x-amazon-apigateway-authorizer: + identitySource: "$request.querystring.param" + type: jwt + jwtConfiguration: + audience: + - MyApi + issuer: https://www.example.com/v1/connect/oidc + openIdConnectUrl: https://www.example.com/v1/connect diff --git a/tests/translator/output/error_http_api_invalid_tags_format.json b/tests/translator/output/error_http_api_invalid_tags_format.json new file mode 100644 index 000000000..7d6a7f17c --- /dev/null +++ b/tests/translator/output/error_http_api_invalid_tags_format.json @@ -0,0 +1,9 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Tags in OpenApi DefinitionBody needs to be a list. {'name': 'tag1'} is a dict not a list.", + "errors": [ + { + "errorMessage": "Structure of the SAM template is invalid. Tags in OpenApi DefinitionBody needs to be a list. {'name': 'tag1'} is a dict not a list." + } + ] +} + \ No newline at end of file From 749634d2e5b88cef4752076c3882949d77591ec3 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Thu, 20 Jan 2022 18:10:51 -0800 Subject: [PATCH 34/59] Remove six as dependency (#2302) --- .pylintrc | 2 +- requirements/base.txt | 2 -- samtranslator/intrinsics/actions.py | 19 ++++++--------- samtranslator/intrinsics/resource_refs.py | 5 +--- samtranslator/model/__init__.py | 6 ++--- samtranslator/model/api/api_generator.py | 10 ++++---- samtranslator/model/api/http_api_generator.py | 7 +++--- samtranslator/model/eventbridge_utils.py | 4 +--- samtranslator/model/eventsources/pull.py | 3 +-- samtranslator/model/eventsources/push.py | 23 +++++++++---------- samtranslator/model/function_policies.py | 4 +--- samtranslator/model/resource_policies.py | 4 +--- .../model/role_utils/role_constructor.py | 4 +--- samtranslator/model/s3_utils/uri_parser.py | 5 ++-- samtranslator/model/sam_resources.py | 5 ++-- samtranslator/model/stepfunctions/events.py | 5 ++-- .../model/stepfunctions/generators.py | 2 -- samtranslator/model/types.py | 3 +-- samtranslator/open_api/open_api.py | 3 +-- .../plugins/api/implicit_http_api_plugin.py | 8 +++---- .../plugins/api/implicit_rest_api_plugin.py | 8 +++---- samtranslator/plugins/globals/globals.py | 3 +-- samtranslator/swagger/swagger.py | 7 +++--- .../translator/logical_id_generator.py | 3 +-- samtranslator/translator/translator.py | 3 +-- samtranslator/yaml_helper.py | 3 +-- tests/sdk/test_template.py | 3 +-- 27 files changed, 56 insertions(+), 98 deletions(-) diff --git a/.pylintrc b/.pylintrc index 685ee7053..6994f5712 100644 --- a/.pylintrc +++ b/.pylintrc @@ -259,7 +259,7 @@ ignore-mixin-members=yes # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis -ignored-modules=six.moves +ignored-modules= # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). diff --git a/requirements/base.txt b/requirements/base.txt index e8c9701a2..060ee4318 100755 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,2 @@ boto3~=1.5 jsonschema~=3.2 -six~=1.15 - diff --git a/samtranslator/intrinsics/actions.py b/samtranslator/intrinsics/actions.py index 7eafbde63..582113587 100644 --- a/samtranslator/intrinsics/actions.py +++ b/samtranslator/intrinsics/actions.py @@ -1,6 +1,5 @@ import re -from six import string_types from samtranslator.utils.py27hash_fix import Py27UniStr from samtranslator.model.exceptions import InvalidTemplateException, InvalidDocumentException @@ -67,7 +66,7 @@ def _parse_resource_reference(cls, ref_value): """ no_result = (None, None) - if not isinstance(ref_value, string_types): + if not isinstance(ref_value, str): return no_result splits = ref_value.split(cls._resource_ref_separator, 1) @@ -98,7 +97,7 @@ def resolve_parameter_refs(self, input_dict, parameters): param_name = input_dict[self.intrinsic_name] - if not isinstance(param_name, string_types): + if not isinstance(param_name, str): return input_dict if param_name in parameters: @@ -153,7 +152,7 @@ def resolve_resource_id_refs(self, input_dict, supported_resource_id_refs): return input_dict ref_value = input_dict[self.intrinsic_name] - if not isinstance(ref_value, string_types) or self._resource_ref_separator in ref_value: + if not isinstance(ref_value, str) or self._resource_ref_separator in ref_value: return input_dict logical_id = ref_value @@ -340,11 +339,11 @@ def _handle_sub_value(self, sub_value, handler_method): # Just handle known references within the string to be substituted and return the whole dictionary # because that's the best we can do here. - if isinstance(sub_value, string_types): + if isinstance(sub_value, str): # Ex: {Fn::Sub: "some string"} sub_value = self._sub_all_refs(sub_value, handler_method) - elif isinstance(sub_value, list) and len(sub_value) > 0 and isinstance(sub_value[0], string_types): + elif isinstance(sub_value, list) and len(sub_value) > 0 and isinstance(sub_value[0], str): # Ex: {Fn::Sub: ["some string", {a:b}] } sub_value[0] = self._sub_all_refs(sub_value[0], handler_method) @@ -495,7 +494,7 @@ def _check_input_value(self, value): # If items in value array is not a string, then following join line will fail. So if any element is not a string # we just pass along the input to CFN for doing the validation for item in value: - if not isinstance(item, string_types): + if not isinstance(item, str): return False return True @@ -553,11 +552,7 @@ def resolve_parameter_refs(self, input_dict, parameters): top_level_key = self.resolve_parameter_refs(value[1], parameters) second_level_key = self.resolve_parameter_refs(value[2], parameters) - if ( - not isinstance(map_name, string_types) - or not isinstance(top_level_key, string_types) - or not isinstance(second_level_key, string_types) - ): + if not isinstance(map_name, str) or not isinstance(top_level_key, str) or not isinstance(second_level_key, str): return input_dict if ( diff --git a/samtranslator/intrinsics/resource_refs.py b/samtranslator/intrinsics/resource_refs.py index 19c8ae682..544c5421a 100644 --- a/samtranslator/intrinsics/resource_refs.py +++ b/samtranslator/intrinsics/resource_refs.py @@ -1,6 +1,3 @@ -from six import string_types - - class SupportedResourceReferences(object): """ Class that contains information about the resource references supported in this SAM template, along with the @@ -32,7 +29,7 @@ def add(self, logical_id, property, value): if not logical_id or not property: raise ValueError("LogicalId and property must be a non-empty string") - if not value or not isinstance(value, string_types): + if not value or not isinstance(value, str): raise ValueError("Property value must be a non-empty string") if logical_id not in self._refs: diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index 892f62e2b..ca0efc59f 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -1,6 +1,4 @@ """ CloudFormation Resource serialization, deserialization, and validation """ -from six import string_types - import re import inspect from samtranslator.model.exceptions import InvalidResourceException @@ -460,7 +458,7 @@ def _resolve_string_parameter(self, intrinsics_resolver, parameter_value, parame return parameter_value value = intrinsics_resolver.resolve_parameter_refs(parameter_value) - if not isinstance(value, string_types) and not isinstance(value, dict): + if not isinstance(value, str) and not isinstance(value, dict): raise InvalidResourceException( self.logical_id, "Could not resolve parameter for '{}' or parameter is not a String.".format(parameter_name), @@ -489,7 +487,7 @@ def __init__(self, *modules): self.resource_types[resource_class.resource_type] = resource_class def can_resolve(self, resource_dict): - if not isinstance(resource_dict, dict) or not isinstance(resource_dict.get("Type"), string_types): + if not isinstance(resource_dict, dict) or not isinstance(resource_dict.get("Type"), str): return False return resource_dict["Type"] in self.resource_types diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 47089a102..e53a2e577 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -1,8 +1,6 @@ import logging from collections import namedtuple -from six import string_types - from samtranslator.metrics.method_decorator import cw_timer from samtranslator.model.intrinsics import ref, fnGetAtt, make_or_condition from samtranslator.model.apigateway import ( @@ -381,7 +379,7 @@ def _construct_stage(self, deployment, swagger, redeploy_restapi_parameters): # If StageName is some intrinsic function, then don't prefix the Stage's logical ID # This will NOT create duplicates because we allow only ONE stage per API resource - stage_name_prefix = self.stage_name if isinstance(self.stage_name, string_types) else "" + stage_name_prefix = self.stage_name if isinstance(self.stage_name, str) else "" if stage_name_prefix.isalnum(): stage_logical_id = self.logical_id + stage_name_prefix + "Stage" else: @@ -481,7 +479,7 @@ def _construct_api_domain(self, rest_api): domain.OwnershipVerificationCertificateArn = self.domain["OwnershipVerificationCertificateArn"] # Create BasepathMappings - if self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), string_types): + if self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), str): basepaths = [self.domain.get("BasePath")] elif self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), list): basepaths = self.domain.get("BasePath") @@ -608,7 +606,7 @@ def _add_cors(self): self.logical_id, "Cors works only with inline Swagger specified in 'DefinitionBody' property." ) - if isinstance(self.cors, string_types) or is_intrinsic(self.cors): + if isinstance(self.cors, str) or is_intrinsic(self.cors): # Just set Origin property. Others will be defaults properties = CorsProperties(AllowOrigin=self.cors) elif isinstance(self.cors, dict): @@ -1129,7 +1127,7 @@ def _set_default_authorizer( if not default_authorizer: return - if not isinstance(default_authorizer, string_types): + if not isinstance(default_authorizer, str): raise InvalidResourceException( self.logical_id, "DefaultAuthorizer is not a string.", diff --git a/samtranslator/model/api/http_api_generator.py b/samtranslator/model/api/http_api_generator.py index 6089b5bb3..d4cd69b08 100644 --- a/samtranslator/model/api/http_api_generator.py +++ b/samtranslator/model/api/http_api_generator.py @@ -1,6 +1,5 @@ import re from collections import namedtuple -from six import string_types from samtranslator.metrics.method_decorator import cw_timer from samtranslator.model.intrinsics import ref, fnGetAtt @@ -294,7 +293,7 @@ def _construct_api_domain(self, http_api): ) # Create BasepathMappings - if self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), string_types): + if self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), str): basepaths = [self.domain.get("BasePath")] elif self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), list): basepaths = self.domain.get("BasePath") @@ -346,7 +345,7 @@ def _construct_basepath_mappings(self, basepaths, http_api): # search for invalid characters in the path and raise error if there are invalid_regex = r"[^0-9a-zA-Z\/\-\_]+" - if not isinstance(path, string_types): + if not isinstance(path, str): raise InvalidResourceException(self.logical_id, "Basepath must be a string.") if re.search(invalid_regex, path) is not None: @@ -580,7 +579,7 @@ def _construct_stage(self): # If StageName is some intrinsic function, then don't prefix the Stage's logical ID # This will NOT create duplicates because we allow only ONE stage per API resource - stage_name_prefix = self.stage_name if isinstance(self.stage_name, string_types) else "" + stage_name_prefix = self.stage_name if isinstance(self.stage_name, str) else "" if stage_name_prefix.isalnum(): stage_logical_id = self.logical_id + stage_name_prefix + "Stage" elif stage_name_prefix == DefaultStageName: diff --git a/samtranslator/model/eventbridge_utils.py b/samtranslator/model/eventbridge_utils.py index c3bd122f3..43a165957 100644 --- a/samtranslator/model/eventbridge_utils.py +++ b/samtranslator/model/eventbridge_utils.py @@ -1,5 +1,3 @@ -from six import string_types - from samtranslator.model.sqs import SQSQueue, SQSQueuePolicy, SQSQueuePolicies from samtranslator.model.exceptions import InvalidEventException @@ -49,7 +47,7 @@ def get_dlq_queue_arn_and_resources(cw_event_source, source_arn, attributes): if dlq_queue_arn is not None: return dlq_queue_arn, [] queue_logical_id = cw_event_source.DeadLetterConfig.get("QueueLogicalId") - if queue_logical_id is not None and not isinstance(queue_logical_id, string_types): + if queue_logical_id is not None and not isinstance(queue_logical_id, str): raise InvalidEventException( cw_event_source.logical_id, "QueueLogicalId must be a string", diff --git a/samtranslator/model/eventsources/pull.py b/samtranslator/model/eventsources/pull.py index 4685ad32c..d8d8b8570 100644 --- a/samtranslator/model/eventsources/pull.py +++ b/samtranslator/model/eventsources/pull.py @@ -1,4 +1,3 @@ -from six import string_types from samtranslator.metrics.method_decorator import cw_timer from samtranslator.model import ResourceMacro, PropertyType from samtranslator.model.eventsources import FUNCTION_EVETSOURCE_METRIC_PREFIX @@ -423,7 +422,7 @@ def validate_uri(self, config, msg): "No {} URI property specified in SourceAccessConfigurations for self managed kafka event.".format(msg), ) - if not isinstance(config.get("URI"), string_types) and not is_intrinsic(config.get("URI")): + if not isinstance(config.get("URI"), str) and not is_intrinsic(config.get("URI")): raise InvalidEventException( self.relative_id, "Wrong Type for {} URI property specified in SourceAccessConfigurations for self managed kafka event.".format( diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index eb3bf5798..19f3c7694 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -1,6 +1,5 @@ import copy import re -from six import string_types from samtranslator.metrics.method_decorator import cw_timer from samtranslator.model import ResourceMacro, PropertyType @@ -257,7 +256,7 @@ class S3(PushEventSource): def resources_to_link(self, resources): if isinstance(self.Bucket, dict) and "Ref" in self.Bucket: bucket_id = self.Bucket["Ref"] - if not isinstance(bucket_id, string_types): + if not isinstance(bucket_id, str): raise InvalidEventException(self.relative_id, "'Ref' value in S3 events is not a valid string.") if bucket_id in resources: return {"bucket": resources[bucket_id], "bucket_id": bucket_id} @@ -326,7 +325,7 @@ def _depend_on_lambda_permissions(self, bucket, permission): depends_on = bucket.get("DependsOn", []) # DependsOn can be either a list of strings or a scalar string - if isinstance(depends_on, string_types): + if isinstance(depends_on, str): depends_on = [depends_on] try: @@ -377,7 +376,7 @@ def _inject_notification_configuration(self, function, bucket): base_event_mapping["Filter"] = self.Filter event_types = self.Events - if isinstance(self.Events, string_types): + if isinstance(self.Events, str): event_types = [self.Events] event_mappings = [] @@ -557,7 +556,7 @@ def resources_to_link(self, resources): stage_suffix = "AllStages" explicit_api = None rest_api_id = self.get_rest_api_id_string(self.RestApiId) - if isinstance(rest_api_id, string_types): + if isinstance(rest_api_id, str): if ( rest_api_id in resources @@ -569,7 +568,7 @@ def resources_to_link(self, resources): permitted_stage = explicit_api["StageName"] # Stage could be a intrinsic, in which case leave the suffix to default value - if isinstance(permitted_stage, string_types): + if isinstance(permitted_stage, str): if not permitted_stage: raise InvalidResourceException(rest_api_id, "StageName cannot be empty.") stage_suffix = permitted_stage @@ -702,7 +701,7 @@ def _add_swagger_integration(self, api, function, intrinsics_resolver): ), ) - if not isinstance(method_authorizer, string_types): + if not isinstance(method_authorizer, str): raise InvalidEventException( self.relative_id, "Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}] " @@ -770,7 +769,7 @@ def _add_swagger_integration(self, api, function, intrinsics_resolver): model=method_model, method=self.Method, path=self.Path ), ) - if not isinstance(method_model, string_types): + if not isinstance(method_model, str): raise InvalidEventException( self.relative_id, "Unable to set RequestModel [{model}] on API method [{method}] for path [{path}] " @@ -855,7 +854,7 @@ def _add_swagger_integration(self, api, function, intrinsics_resolver): parameters.append(settings) - elif isinstance(parameter, string_types): + elif isinstance(parameter, str): if not re.match("method\.request\.(querystring|path|header)\.", parameter): raise InvalidEventException( self.relative_id, @@ -970,7 +969,7 @@ class Cognito(PushEventSource): def resources_to_link(self, resources): if isinstance(self.UserPool, dict) and "Ref" in self.UserPool: userpool_id = self.UserPool["Ref"] - if not isinstance(userpool_id, string_types): + if not isinstance(userpool_id, str): raise InvalidEventException( self.logical_id, "Ref in Userpool is not a string.", @@ -1012,7 +1011,7 @@ def to_cloudformation(self, **kwargs): def _inject_lambda_config(self, function, userpool): event_triggers = self.Trigger - if isinstance(self.Trigger, string_types): + if isinstance(self.Trigger, str): event_triggers = [self.Trigger] # TODO can these be conditional? @@ -1197,7 +1196,7 @@ def _add_auth_to_openapi_integration(self, api, editor): """ method_authorizer = self.Auth.get("Authorizer") - if method_authorizer is not None and not isinstance(method_authorizer, string_types): + if method_authorizer is not None and not isinstance(method_authorizer, str): raise InvalidEventException( self.relative_id, "'Authorizer' in the 'Auth' section must be a string.", diff --git a/samtranslator/model/function_policies.py b/samtranslator/model/function_policies.py index 1866cd992..0991cd59b 100644 --- a/samtranslator/model/function_policies.py +++ b/samtranslator/model/function_policies.py @@ -1,8 +1,6 @@ from enum import Enum from collections import namedtuple -from six import string_types - from samtranslator.model.intrinsics import ( is_intrinsic, is_intrinsic_if, @@ -123,7 +121,7 @@ def _get_type(self, policy): # Must handle intrinsic functions. Policy could be a primitive type or an intrinsic function # Managed policies are of type string - if isinstance(policy, string_types): + if isinstance(policy, str): return PolicyTypes.MANAGED_POLICY # Handle the special case for 'if' intrinsic function diff --git a/samtranslator/model/resource_policies.py b/samtranslator/model/resource_policies.py index 7bf3a23b1..c67fca0c2 100644 --- a/samtranslator/model/resource_policies.py +++ b/samtranslator/model/resource_policies.py @@ -1,8 +1,6 @@ from enum import Enum from collections import namedtuple -from six import string_types - from samtranslator.model.intrinsics import ( is_intrinsic, is_intrinsic_if, @@ -123,7 +121,7 @@ def _get_type(self, policy): # Must handle intrinsic functions. Policy could be a primitive type or an intrinsic function # Managed policies are of type string - if isinstance(policy, string_types): + if isinstance(policy, str): return PolicyTypes.MANAGED_POLICY # Handle the special case for 'if' intrinsic function diff --git a/samtranslator/model/role_utils/role_constructor.py b/samtranslator/model/role_utils/role_constructor.py index 560101849..79845a101 100644 --- a/samtranslator/model/role_utils/role_constructor.py +++ b/samtranslator/model/role_utils/role_constructor.py @@ -1,5 +1,3 @@ -from six import string_types - from samtranslator.model.iam import IAMRole from samtranslator.model.resource_policies import ResourcePolicies, PolicyTypes from samtranslator.model.intrinsics import is_intrinsic_if, is_intrinsic_no_value @@ -86,7 +84,7 @@ def construct_role_for_resource( # policy_arn = policy_entry.data - if isinstance(policy_entry.data, string_types) and policy_entry.data in managed_policy_map: + if isinstance(policy_entry.data, str) and policy_entry.data in managed_policy_map: policy_arn = managed_policy_map[policy_entry.data] # De-Duplicate managed policy arns before inserting. Mainly useful diff --git a/samtranslator/model/s3_utils/uri_parser.py b/samtranslator/model/s3_utils/uri_parser.py index f94bec231..d889a0cea 100644 --- a/samtranslator/model/s3_utils/uri_parser.py +++ b/samtranslator/model/s3_utils/uri_parser.py @@ -1,5 +1,4 @@ -from six import string_types -from six.moves.urllib.parse import urlparse, parse_qs +from urllib.parse import urlparse, parse_qs from samtranslator.model.exceptions import InvalidResourceException @@ -9,7 +8,7 @@ def parse_s3_uri(uri): :return: a BodyS3Location dict or None if not an S3 Uri :rtype: dict """ - if not isinstance(uri, string_types): + if not isinstance(uri, str): return None url = urlparse(uri) diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 3d18076bb..5e6c582f3 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -1,5 +1,4 @@ """ SAM macro definitions """ -from six import string_types import copy import samtranslator.model.eventsources @@ -157,7 +156,7 @@ def to_cloudformation(self, **kwargs): code_sha256 = None if self.AutoPublishCodeSha256: code_sha256 = intrinsics_resolver.resolve_parameter_refs(self.AutoPublishCodeSha256) - if not isinstance(code_sha256, string_types): + if not isinstance(code_sha256, str): raise InvalidResourceException( self.logical_id, "AutoPublishCodeSha256 must be a string", @@ -389,7 +388,7 @@ def _get_resolved_alias_name(self, property_name, original_alias_value, intrinsi # Try to resolve. resolved_alias_name = intrinsics_resolver.resolve_parameter_refs(original_alias_value) - if not isinstance(resolved_alias_name, string_types): + if not isinstance(resolved_alias_name, str): # This is still a dictionary which means we are not able to completely resolve intrinsics raise InvalidResourceException( self.logical_id, "'{}' must be a string or a Ref to a template parameter".format(property_name) diff --git a/samtranslator/model/stepfunctions/events.py b/samtranslator/model/stepfunctions/events.py index d45c04cb3..a14d6cdbd 100644 --- a/samtranslator/model/stepfunctions/events.py +++ b/samtranslator/model/stepfunctions/events.py @@ -1,4 +1,3 @@ -from six import string_types import json from samtranslator.metrics.method_decorator import cw_timer @@ -261,7 +260,7 @@ def resources_to_link(self, resources): stage_suffix = "AllStages" explicit_api = None rest_api_id = PushApi.get_rest_api_id_string(self.RestApiId) - if isinstance(rest_api_id, string_types): + if isinstance(rest_api_id, str): if ( rest_api_id in resources @@ -273,7 +272,7 @@ def resources_to_link(self, resources): permitted_stage = explicit_api["StageName"] # Stage could be a intrinsic, in which case leave the suffix to default value - if isinstance(permitted_stage, string_types): + if isinstance(permitted_stage, str): stage_suffix = permitted_stage else: stage_suffix = "Stage" diff --git a/samtranslator/model/stepfunctions/generators.py b/samtranslator/model/stepfunctions/generators.py index 00bdd66ae..682fee5ae 100644 --- a/samtranslator/model/stepfunctions/generators.py +++ b/samtranslator/model/stepfunctions/generators.py @@ -2,8 +2,6 @@ from uuid import uuid4 from copy import deepcopy -from six import string_types - import samtranslator.model.eventsources.push from samtranslator.metrics.method_decorator import cw_timer from samtranslator.model import ResourceTypeResolver diff --git a/samtranslator/model/types.py b/samtranslator/model/types.py index 36b44257f..8f78f0261 100644 --- a/samtranslator/model/types.py +++ b/samtranslator/model/types.py @@ -8,7 +8,6 @@ the Permissions property is an ARN or list of ARNs. In this situation, we validate that the Permissions property is either a string or a list of strings, but do not validate whether the string(s) are valid IAM policy ARNs. """ -from six import string_types import samtranslator.model.exceptions @@ -123,7 +122,7 @@ def is_str(): :returns: a string validator :rtype: callable """ - return is_type(string_types) + return is_type(str) def any_type(): diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index 18d5d1267..2a6e650ab 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -1,6 +1,5 @@ import copy import re -from six import string_types from samtranslator.model.intrinsics import ref from samtranslator.model.intrinsics import make_conditional @@ -652,7 +651,7 @@ def _normalize_method_name(method): :param string method: Name of the HTTP Method :return string: Normalized method name """ - if not method or not isinstance(method, string_types): + if not method or not isinstance(method, str): return method method = method.lower() diff --git a/samtranslator/plugins/api/implicit_http_api_plugin.py b/samtranslator/plugins/api/implicit_http_api_plugin.py index 5012ac11f..8ea3a6cd0 100644 --- a/samtranslator/plugins/api/implicit_http_api_plugin.py +++ b/samtranslator/plugins/api/implicit_http_api_plugin.py @@ -1,5 +1,3 @@ -import six - from samtranslator.model.intrinsics import make_conditional from samtranslator.model.naming import GeneratedLogicalId from samtranslator.plugins.api.implicit_api_plugin import ImplicitApiPlugin @@ -83,12 +81,12 @@ def _process_api_events( key = "Path" if not path else "Method" raise InvalidEventException(logicalId, "Event is missing key '{}'.".format(key)) - if not isinstance(path, six.string_types) or not isinstance(method, six.string_types): - key = "Path" if not isinstance(path, six.string_types) else "Method" + if not isinstance(path, str) or not isinstance(method, str): + key = "Path" if not isinstance(path, str) else "Method" raise InvalidEventException(logicalId, "Api Event must have a String specified for '{}'.".format(key)) # !Ref is resolved by this time. If it is not a string, we can't parse/use this Api. - if api_id and not isinstance(api_id, six.string_types): + if api_id and not isinstance(api_id, str): raise InvalidEventException( logicalId, "Api Event's ApiId must be a string referencing an Api in the same template." ) diff --git a/samtranslator/plugins/api/implicit_rest_api_plugin.py b/samtranslator/plugins/api/implicit_rest_api_plugin.py index 5636abbf2..d0ec8fc38 100644 --- a/samtranslator/plugins/api/implicit_rest_api_plugin.py +++ b/samtranslator/plugins/api/implicit_rest_api_plugin.py @@ -1,5 +1,3 @@ -import six - from samtranslator.model.naming import GeneratedLogicalId from samtranslator.plugins.api.implicit_api_plugin import ImplicitApiPlugin from samtranslator.public.swagger import SwaggerEditor @@ -80,13 +78,13 @@ def _process_api_events( except KeyError as e: raise InvalidEventException(logicalId, "Event is missing key {}.".format(e)) - if not isinstance(path, six.string_types): + if not isinstance(path, str): raise InvalidEventException(logicalId, "Api Event must have a String specified for 'Path'.") - if not isinstance(method, six.string_types): + if not isinstance(method, str): raise InvalidEventException(logicalId, "Api Event must have a String specified for 'Method'.") # !Ref is resolved by this time. If it is not a string, we can't parse/use this Api. - if api_id and not isinstance(api_id, six.string_types): + if api_id and not isinstance(api_id, str): raise InvalidEventException( logicalId, "Api Event's RestApiId must be a string referencing an Api in the same template." ) diff --git a/samtranslator/plugins/globals/globals.py b/samtranslator/plugins/globals/globals.py index 950b5583c..0b2680769 100644 --- a/samtranslator/plugins/globals/globals.py +++ b/samtranslator/plugins/globals/globals.py @@ -1,7 +1,6 @@ from samtranslator.public.sdk.resource import SamResourceType from samtranslator.public.intrinsics import is_intrinsics from samtranslator.swagger.swagger import SwaggerEditor -from six import string_types class Globals(object): @@ -156,7 +155,7 @@ def fix_openapi_definitions(cls, template): SwaggerEditor.get_openapi_version_3_regex(), properties[cls._OPENAPIVERSION] ) ): - if not isinstance(properties[cls._OPENAPIVERSION], string_types): + if not isinstance(properties[cls._OPENAPIVERSION], str): properties[cls._OPENAPIVERSION] = str(properties[cls._OPENAPIVERSION]) resource["Properties"] = properties if "DefinitionBody" in properties: diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 1466e7329..f7f49ac27 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -1,7 +1,6 @@ import copy import json import re -from six import string_types from samtranslator.model.intrinsics import ref from samtranslator.model.intrinsics import make_conditional, fnSub @@ -381,7 +380,7 @@ def replace_recursively(bmt): return to_return if isinstance(bmt, list): return [replace_recursively(item) for item in bmt] - if isinstance(bmt, string_types) or isinstance(bmt, Py27UniStr): + if isinstance(bmt, str) or isinstance(bmt, Py27UniStr): return Py27UniStr(bmt.replace("~1", "/")) return bmt @@ -1377,7 +1376,7 @@ def _normalize_method_name(method): :param string method: Name of the HTTP Method :return string: Normalized method name """ - if not method or not isinstance(method, string_types): + if not method or not isinstance(method, str): return method method = method.lower() @@ -1437,7 +1436,7 @@ def _validate_list_property_is_resolved(property_list): :return bool: True if the property_list is all of type string otherwise False """ - if property_list is not None and not all(isinstance(x, string_types) for x in property_list): + if property_list is not None and not all(isinstance(x, str) for x in property_list): return False return True diff --git a/samtranslator/translator/logical_id_generator.py b/samtranslator/translator/logical_id_generator.py index e87fc42c3..45103e473 100644 --- a/samtranslator/translator/logical_id_generator.py +++ b/samtranslator/translator/logical_id_generator.py @@ -1,7 +1,6 @@ import hashlib import json import sys -from six import string_types class LogicalIdGenerator(object): @@ -88,7 +87,7 @@ def _stringify(self, data): :return: string representation of the dictionary :rtype string """ - if isinstance(data, string_types): + if isinstance(data, str): return data # Get the most compact dictionary (separators) and sort the keys recursively to get a stable output diff --git a/samtranslator/translator/translator.py b/samtranslator/translator/translator.py index a447cd64a..3edc4ed14 100644 --- a/samtranslator/translator/translator.py +++ b/samtranslator/translator/translator.py @@ -1,5 +1,4 @@ import copy -from six import string_types from samtranslator.metrics.method_decorator import MetricsMethodWrapperSingleton from samtranslator.metrics.metrics import DummyMetricsPublisher, Metrics @@ -70,7 +69,7 @@ def _get_function_names(self, resource_dict, intrinsics_resolver): if item.get("Type") == "Api" and item.get("Properties") and item.get("Properties").get("RestApiId"): rest_api = item.get("Properties").get("RestApiId") api_name = Api.get_rest_api_id_string(rest_api) - if isinstance(api_name, string_types): + if isinstance(api_name, str): resource_dict_copy = copy.deepcopy(resource_dict) function_name = intrinsics_resolver.resolve_parameter_refs( resource_dict_copy.get("Properties").get("FunctionName") diff --git a/samtranslator/yaml_helper.py b/samtranslator/yaml_helper.py index 67fc33f09..60e15e05a 100644 --- a/samtranslator/yaml_helper.py +++ b/samtranslator/yaml_helper.py @@ -1,6 +1,5 @@ import yaml from yaml import ScalarNode, SequenceNode -from six import string_types # This helper copied almost entirely from # https://github.com/aws/aws-cli/blob/develop/awscli/customizations/cloudformation/yamlhelper.py @@ -28,7 +27,7 @@ def intrinsics_multi_constructor(loader, tag_prefix, node): cfntag = prefix + tag - if tag == "GetAtt" and isinstance(node.value, string_types): + if tag == "GetAtt" and isinstance(node.value, str): # ShortHand notation for !GetAtt accepts Resource.Attribute format # while the standard notation is to use an array # [Resource, Attribute]. Convert shorthand to standard format diff --git a/tests/sdk/test_template.py b/tests/sdk/test_template.py index 64e5247b7..007478bef 100644 --- a/tests/sdk/test_template.py +++ b/tests/sdk/test_template.py @@ -1,5 +1,4 @@ from unittest import TestCase -from six import assertCountEqual from samtranslator.sdk.template import SamTemplate from samtranslator.sdk.resource import SamResource @@ -31,7 +30,7 @@ def test_iterate_must_yield_sam_resources_only(self): ] actual = [(id, resource.to_dict()) for id, resource in template.iterate()] - assertCountEqual(self, expected, actual) + self.assertCountEqual(expected, actual) def test_iterate_must_filter_by_resource_type(self): From ac27136c6c2b6213c1d7c032ecf91e0b87cd63d2 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Fri, 21 Jan 2022 08:44:35 -0800 Subject: [PATCH 35/59] Fix all warnings in tests (#2303) * Fix all warnings in tests * Black reformat --- pytest.ini | 2 ++ requirements/dev.txt | 1 + samtranslator/intrinsics/actions.py | 4 ++-- samtranslator/metrics/metrics.py | 4 +++- samtranslator/model/eventsources/push.py | 4 ++-- samtranslator/open_api/open_api.py | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pytest.ini b/pytest.ini index 16c9ed73e..257206172 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,4 +5,6 @@ addopts = --cov samtranslator --cov-report term-missing --cov-fail-under 95 testpaths = tests env = AWS_DEFAULT_REGION = ap-southeast-1 +markers = + slow: marks tests as slow (deselect with '-m "not slow"') diff --git a/requirements/dev.txt b/requirements/dev.txt index eebaee996..72ee04f7b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -3,6 +3,7 @@ flake8~=3.8.4 tox~=3.24 pytest-cov~=2.10.1 pytest-xdist~=2.5 +pytest-env~=0.6.2 pylint>=1.7.2,<2.0 pyyaml~=5.4 diff --git a/samtranslator/intrinsics/actions.py b/samtranslator/intrinsics/actions.py index 582113587..1bf9b2acf 100644 --- a/samtranslator/intrinsics/actions.py +++ b/samtranslator/intrinsics/actions.py @@ -370,8 +370,8 @@ def handler_method(full_ref, ref_value): """ # RegExp to find pattern "${logicalId.property}" and return the word inside bracket - logical_id_regex = "[A-Za-z0-9\.]+|AWS::[A-Z][A-Za-z]*" - ref_pattern = re.compile(r"\$\{(" + logical_id_regex + ")\}") + logical_id_regex = r"[A-Za-z0-9\.]+|AWS::[A-Z][A-Za-z]*" + ref_pattern = re.compile(r"\$\{(" + logical_id_regex + r")\}") # Find all the pattern, and call the handler to decide how to substitute them. # Do the substitution and return the final text diff --git a/samtranslator/metrics/metrics.py b/samtranslator/metrics/metrics.py index e75a1cf47..7e5781c26 100644 --- a/samtranslator/metrics/metrics.py +++ b/samtranslator/metrics/metrics.py @@ -125,7 +125,9 @@ def __init__(self, namespace="ServerlessTransform", metrics_publisher=None): def __del__(self): if len(self.metrics_cache) > 0: # attempting to publish if user forgot to call publish in code - LOG.warn("There are unpublished metrics. Please make sure you call publish after you record all metrics.") + LOG.warning( + "There are unpublished metrics. Please make sure you call publish after you record all metrics." + ) self.publish() def _record_metric(self, name, value, unit, dimensions=None): diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 19f3c7694..20f862707 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -831,7 +831,7 @@ def _add_swagger_integration(self, api, function, intrinsics_resolver): parameter_name, parameter_value = next(iter(parameter.items())) - if not re.match("method\.request\.(querystring|path|header)\.", parameter_name): + if not re.match(r"method\.request\.(querystring|path|header)\.", parameter_name): raise InvalidEventException( self.relative_id, "Invalid value for 'RequestParameters' property. Keys must be in the format " @@ -855,7 +855,7 @@ def _add_swagger_integration(self, api, function, intrinsics_resolver): parameters.append(settings) elif isinstance(parameter, str): - if not re.match("method\.request\.(querystring|path|header)\.", parameter): + if not re.match(r"method\.request\.(querystring|path|header)\.", parameter): raise InvalidEventException( self.relative_id, "Invalid value for 'RequestParameters' property. Keys must be in the format " diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index 2a6e650ab..d67f7aac9 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -107,7 +107,7 @@ def get_integration_function_logical_id(self, path_name, method_name): arn = uri.get("Fn::Sub", "") # Extract lambda integration (${LambdaName.Arn}) and split ".Arn" off from it - regex = "([A-Za-z0-9]+\.Arn)" + regex = r"([A-Za-z0-9]+\.Arn)" matches = re.findall(regex, arn) # Prevent IndexError when integration URI doesn't contain .Arn (e.g. a Function with # AutoPublishAlias translates to AWS::Lambda::Alias, which make_shorthand represents From 7eb358731933152df41c56b2e8ce1916186460e1 Mon Sep 17 00:00:00 2001 From: Wilton_ Date: Fri, 21 Jan 2022 08:46:01 -0800 Subject: [PATCH 36/59] fix: Raise Invalid Resource When DisableExecuteApiEndpoint: False And Has No DefinitionBody (#2285) * Added Python3 Support for Translate CLI * Fixed disable_execute_api_endpoint: false Case * Formatted with Black * Added Fix for HttpApi * Added Unit Tests * Removed Function from Test Template * Updated Executable to be Python 3 Only --- bin/sam-translate.py | 4 +++- samtranslator/model/api/api_generator.py | 2 +- samtranslator/model/api/http_api_generator.py | 2 +- .../error_api_with_disable_api_execute_endpoint_false.yaml | 7 +++++++ ...r_http_api_with_disable_api_execute_endpoint_false.yaml | 7 +++++++ .../error_api_with_disable_api_execute_endpoint_false.json | 3 +++ ...r_http_api_with_disable_api_execute_endpoint_false.json | 3 +++ 7 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/translator/input/error_api_with_disable_api_execute_endpoint_false.yaml create mode 100644 tests/translator/input/error_http_api_with_disable_api_execute_endpoint_false.yaml create mode 100644 tests/translator/output/error_api_with_disable_api_execute_endpoint_false.json create mode 100644 tests/translator/output/error_http_api_with_disable_api_execute_endpoint_false.json diff --git a/bin/sam-translate.py b/bin/sam-translate.py index 3e961031b..e533e2949 100755 --- a/bin/sam-translate.py +++ b/bin/sam-translate.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python """Convert SAM templates to CloudFormation templates. @@ -26,7 +26,9 @@ import sys import boto3 + from docopt import docopt +from functools import reduce my_path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, my_path + "/..") diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index e53a2e577..675dd1aca 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -304,7 +304,7 @@ def _add_endpoint_extension(self): https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-definitionbody For this reason, we always put DisableExecuteApiEndpoint into openapi object irrespective of origin of DefinitionBody. """ - if self.disable_execute_api_endpoint and not self.definition_body: + if self.disable_execute_api_endpoint is not None and not self.definition_body: raise InvalidResourceException( self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property." ) diff --git a/samtranslator/model/api/http_api_generator.py b/samtranslator/model/api/http_api_generator.py index d4cd69b08..d033a75bd 100644 --- a/samtranslator/model/api/http_api_generator.py +++ b/samtranslator/model/api/http_api_generator.py @@ -141,7 +141,7 @@ def _add_endpoint_configuration(self): For this reason, we always put DisableExecuteApiEndpoint into openapi object. """ - if self.disable_execute_api_endpoint and not self.definition_body: + if self.disable_execute_api_endpoint is not None and not self.definition_body: raise InvalidResourceException( self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property." ) diff --git a/tests/translator/input/error_api_with_disable_api_execute_endpoint_false.yaml b/tests/translator/input/error_api_with_disable_api_execute_endpoint_false.yaml new file mode 100644 index 000000000..d419bc577 --- /dev/null +++ b/tests/translator/input/error_api_with_disable_api_execute_endpoint_false.yaml @@ -0,0 +1,7 @@ +Resources: + ApiGatewayApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + DisableExecuteApiEndpoint: False + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json \ No newline at end of file diff --git a/tests/translator/input/error_http_api_with_disable_api_execute_endpoint_false.yaml b/tests/translator/input/error_http_api_with_disable_api_execute_endpoint_false.yaml new file mode 100644 index 000000000..64e4eb7fe --- /dev/null +++ b/tests/translator/input/error_http_api_with_disable_api_execute_endpoint_false.yaml @@ -0,0 +1,7 @@ +Resources: + ApiGatewayApi: + Type: AWS::Serverless::HttpApi + Properties: + StageName: prod + DisableExecuteApiEndpoint: False + DefinitionUri: s3://sam-demo-bucket/webpage_swagger.json \ No newline at end of file diff --git a/tests/translator/output/error_api_with_disable_api_execute_endpoint_false.json b/tests/translator/output/error_api_with_disable_api_execute_endpoint_false.json new file mode 100644 index 000000000..5adf87eaf --- /dev/null +++ b/tests/translator/output/error_api_with_disable_api_execute_endpoint_false.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [ApiGatewayApi] is invalid. DisableExecuteApiEndpoint works only within 'DefinitionBody' property." +} \ No newline at end of file diff --git a/tests/translator/output/error_http_api_with_disable_api_execute_endpoint_false.json b/tests/translator/output/error_http_api_with_disable_api_execute_endpoint_false.json new file mode 100644 index 000000000..5adf87eaf --- /dev/null +++ b/tests/translator/output/error_http_api_with_disable_api_execute_endpoint_false.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [ApiGatewayApi] is invalid. DisableExecuteApiEndpoint works only within 'DefinitionBody' property." +} \ No newline at end of file From 59df2db23d5ed4a2195bf9e563d1d2a648b1bac1 Mon Sep 17 00:00:00 2001 From: Ruperto Torres <86501267+torresxb1@users.noreply.github.com> Date: Mon, 24 Jan 2022 13:26:06 -0800 Subject: [PATCH 37/59] handle 'Invalid Swagger Document' and refactor some validation into Swagger Editor constructor (#2263) * handle 'Invalid Swagger Document' and refactor some validation into Swagger Editor constructor * missed removing a comment * move path item validation into method * update new function to use swagger editor instance * small change, changing test to use swagger instead of openapi * update deployment hashes in some updated tests * add docstring :raises --- samtranslator/model/api/api_generator.py | 80 ++++--------------- samtranslator/swagger/swagger.py | 44 +++++++--- tests/model/api/test_api_generator.py | 4 +- tests/swagger/test_swagger.py | 8 +- .../input/api_with_resource_refs.yaml | 4 +- ...finition_body_invalid_openapi_version.yaml | 13 +++ ...ror_api_definition_body_invalid_paths.yaml | 11 +++ ...ition_body_missing_openapi_or_swagger.yaml | 12 +++ ...ror_api_definition_body_missing_paths.yaml | 10 +++ .../input/error_api_invalid_auth.yaml | 8 -- .../error_api_invalid_definitionuri.yaml | 4 +- .../error_api_invalid_request_model.yaml | 12 --- tests/translator/input/explicit_api.yaml | 4 +- .../input/explicit_api_openapi_3.yaml | 4 +- .../output/api_with_resource_refs.json | 12 +-- .../output/aws-cn/api_with_resource_refs.json | 12 +-- .../output/aws-cn/explicit_api.json | 10 +-- .../output/aws-cn/explicit_api_openapi_3.json | 10 +-- .../aws-us-gov/api_with_resource_refs.json | 12 +-- .../output/aws-us-gov/explicit_api.json | 10 +-- .../aws-us-gov/explicit_api_openapi_3.json | 10 +-- ...finition_body_invalid_openapi_version.json | 1 + ...ror_api_definition_body_invalid_paths.json | 1 + ...ition_body_missing_openapi_or_swagger.json | 1 + ...ror_api_definition_body_missing_paths.json | 1 + .../output/error_api_invalid_auth.json | 2 +- .../error_api_invalid_request_model.json | 2 +- tests/translator/output/explicit_api.json | 10 +-- .../output/explicit_api_openapi_3.json | 10 +-- tests/translator/test_translator.py | 3 +- 30 files changed, 166 insertions(+), 159 deletions(-) create mode 100644 tests/translator/input/error_api_definition_body_invalid_openapi_version.yaml create mode 100644 tests/translator/input/error_api_definition_body_invalid_paths.yaml create mode 100644 tests/translator/input/error_api_definition_body_missing_openapi_or_swagger.yaml create mode 100644 tests/translator/input/error_api_definition_body_missing_paths.yaml create mode 100644 tests/translator/output/error_api_definition_body_invalid_openapi_version.json create mode 100644 tests/translator/output/error_api_definition_body_invalid_paths.json create mode 100644 tests/translator/output/error_api_definition_body_missing_openapi_or_swagger.json create mode 100644 tests/translator/output/error_api_definition_body_missing_paths.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 675dd1aca..212092696 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -236,6 +236,8 @@ def __init__( self.template_conditions = template_conditions self.mode = mode + self.swagger_editor = SwaggerEditor(self.definition_body) if self.definition_body else None + def _construct_rest_api(self): """Constructs and returns the ApiGateway RestApi. @@ -282,7 +284,7 @@ def _construct_rest_api(self): rest_api.BodyS3Location = self._construct_body_s3_dict() elif self.definition_body: # # Post Process OpenApi Auth Settings - self.definition_body = self._openapi_postprocess(self.definition_body) + self.definition_body = self._openapi_postprocess(self.swagger_editor.swagger) rest_api.Body = self.definition_body if self.name: @@ -308,9 +310,7 @@ def _add_endpoint_extension(self): raise InvalidResourceException( self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property." ) - editor = SwaggerEditor(self.definition_body) - editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint) - self.definition_body = editor.swagger + self.swagger_editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint) def _construct_body_s3_dict(self): """Constructs the RestApi's `BodyS3Location property`_, from the SAM Api's DefinitionUri property. @@ -620,13 +620,6 @@ def _add_cors(self): else: raise InvalidResourceException(self.logical_id, INVALID_ERROR) - if not SwaggerEditor.is_valid(self.definition_body): - raise InvalidResourceException( - self.logical_id, - "Unable to add Cors configuration because " - "'DefinitionBody' does not contain a valid Swagger definition.", - ) - if properties.AllowCredentials is True and properties.AllowOrigin == _CORS_WILDCARD: raise InvalidResourceException( self.logical_id, @@ -635,10 +628,9 @@ def _add_cors(self): "'AllowOrigin' is \"'*'\" or not set", ) - editor = SwaggerEditor(self.definition_body) - for path in editor.iter_on_path(): + for path in self.swagger_editor.iter_on_path(): try: - editor.add_cors( + self.swagger_editor.add_cors( path, properties.AllowOrigin, properties.AllowHeaders, @@ -649,9 +641,6 @@ def _add_cors(self): except InvalidTemplateException as ex: raise InvalidResourceException(self.logical_id, ex.message) - # Assign the Swagger back to template - self.definition_body = editor.swagger - def _add_binary_media_types(self): """ Add binary media types to Swagger @@ -664,11 +653,7 @@ def _add_binary_media_types(self): if self.binary_media and not self.definition_body: return - editor = SwaggerEditor(self.definition_body) - editor.add_binary_media_types(self.binary_media) - - # Assign the Swagger back to template - self.definition_body = editor.swagger + self.swagger_editor.add_binary_media_types(self.binary_media) def _add_auth(self): """ @@ -687,20 +672,13 @@ def _add_auth(self): if not all(key in AuthProperties._fields for key in self.auth.keys()): raise InvalidResourceException(self.logical_id, "Invalid value for 'Auth' property") - if not SwaggerEditor.is_valid(self.definition_body): - raise InvalidResourceException( - self.logical_id, - "Unable to add Auth configuration because " - "'DefinitionBody' does not contain a valid Swagger definition.", - ) - swagger_editor = SwaggerEditor(self.definition_body) auth_properties = AuthProperties(**self.auth) authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer) if authorizers: - swagger_editor.add_authorizers_security_definitions(authorizers) + self.swagger_editor.add_authorizers_security_definitions(authorizers) self._set_default_authorizer( - swagger_editor, + self.swagger_editor, authorizers, auth_properties.DefaultAuthorizer, auth_properties.AddDefaultAuthorizerToCorsPreflight, @@ -708,19 +686,17 @@ def _add_auth(self): ) if auth_properties.ApiKeyRequired: - swagger_editor.add_apikey_security_definition() - self._set_default_apikey_required(swagger_editor) + self.swagger_editor.add_apikey_security_definition() + self._set_default_apikey_required(self.swagger_editor) if auth_properties.ResourcePolicy: SwaggerEditor.validate_is_dict( auth_properties.ResourcePolicy, "ResourcePolicy must be a map (ResourcePolicyStatement)." ) - for path in swagger_editor.iter_on_path(): - swagger_editor.add_resource_policy(auth_properties.ResourcePolicy, path, self.stage_name) + for path in self.swagger_editor.iter_on_path(): + self.swagger_editor.add_resource_policy(auth_properties.ResourcePolicy, path, self.stage_name) if auth_properties.ResourcePolicy.get("CustomStatements"): - swagger_editor.add_custom_statements(auth_properties.ResourcePolicy.get("CustomStatements")) - - self.definition_body = self._openapi_postprocess(swagger_editor.swagger) + self.swagger_editor.add_custom_statements(auth_properties.ResourcePolicy.get("CustomStatements")) def _construct_usage_plan(self, rest_api_stage=None): """Constructs and returns the ApiGateway UsagePlan, ApiGateway UsagePlanKey, ApiGateway ApiKey for Auth. @@ -923,15 +899,6 @@ def _add_gateway_responses(self): ), ) - if not SwaggerEditor.is_valid(self.definition_body): - raise InvalidResourceException( - self.logical_id, - "Unable to add Auth configuration because " - "'DefinitionBody' does not contain a valid Swagger definition.", - ) - - swagger_editor = SwaggerEditor(self.definition_body) - # The dicts below will eventually become part of swagger/openapi definition, thus requires using Py27Dict() gateway_responses = Py27Dict() for response_type, response in self.gateway_responses.items(): @@ -943,10 +910,7 @@ def _add_gateway_responses(self): ) if gateway_responses: - swagger_editor.add_gateway_responses(gateway_responses) - - # Assign the Swagger back to template - self.definition_body = swagger_editor.swagger + self.swagger_editor.add_gateway_responses(gateway_responses) def _add_models(self): """ @@ -962,22 +926,10 @@ def _add_models(self): self.logical_id, "Models works only with inline Swagger specified in " "'DefinitionBody' property." ) - if not SwaggerEditor.is_valid(self.definition_body): - raise InvalidResourceException( - self.logical_id, - "Unable to add Models definitions because " - "'DefinitionBody' does not contain a valid Swagger definition.", - ) - if not all(isinstance(model, dict) for model in self.models.values()): raise InvalidResourceException(self.logical_id, "Invalid value for 'Models' property") - swagger_editor = SwaggerEditor(self.definition_body) - swagger_editor.add_models(self.models) - - # Assign the Swagger back to template - - self.definition_body = self._openapi_postprocess(swagger_editor.swagger) + self.swagger_editor.add_models(self.models) def _openapi_postprocess(self, definition_body): """ diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index f7f49ac27..0fce61221 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -46,19 +46,13 @@ def __init__(self, doc): modifications on this copy. :param dict doc: Swagger document as a dictionary - :raises ValueError: If the input Swagger document does not meet the basic Swagger requirements. + :raises InvalidDocumentException if doc is invalid """ - if not SwaggerEditor.is_valid(doc): - raise ValueError("Invalid Swagger document") - self._doc = copy.deepcopy(doc) - self.paths = self._doc["paths"] - self.security_definitions = self._doc.get("securityDefinitions", Py27Dict()) - self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, Py27Dict()) - self.resource_policy = self._doc.get(self._X_APIGW_POLICY, Py27Dict()) - self.definitions = self._doc.get("definitions", Py27Dict()) + self.validate_definition_body(doc) + self.paths = self._doc.get("paths") # https://swagger.io/specification/#path-item-object # According to swagger spec, # each path item object must be a dict (even it is empty). @@ -67,6 +61,11 @@ def __init__(self, doc): for path in self.iter_on_path(): SwaggerEditor.validate_path_item_is_dict(self.get_path(path), path) + self.security_definitions = self._doc.get("securityDefinitions", Py27Dict()) + self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, Py27Dict()) + self.resource_policy = self._doc.get(self._X_APIGW_POLICY, Py27Dict()) + self.definitions = self._doc.get("definitions", Py27Dict()) + def get_path(self, path): path_dict = self.paths.get(path) if isinstance(path_dict, dict) and self._CONDITIONAL_IF in path_dict: @@ -162,7 +161,6 @@ def add_path(self, path, method=None): :param string path: Path name :param string method: HTTP method - :raises ValueError: If the value of `path` in Swagger is not a dictionary """ method = self._normalize_method_name(method) @@ -1310,6 +1308,32 @@ def is_valid(data): ) return False + def validate_definition_body(self, definition_body): + """ + Checks if definition_body is a valid Swagger document + + :param dict definition_body: Data to be validated + :raises InvalidDocumentException if definition_body is invalid + """ + + SwaggerEditor.validate_is_dict(definition_body, "DefinitionBody must be a dictionary.") + SwaggerEditor.validate_is_dict( + definition_body.get("paths"), "The 'paths' property of DefinitionBody must be present and be a dictionary." + ) + + has_swagger = definition_body.get("swagger") + has_openapi3 = definition_body.get("openapi") and SwaggerEditor.safe_compare_regex_with_string( + SwaggerEditor.get_openapi_version_3_regex(), definition_body["openapi"] + ) + if not (has_swagger) and not (has_openapi3): + raise InvalidDocumentException( + [ + InvalidTemplateException( + "DefinitionBody must have either: (1) a 'swagger' property or (2) an 'openapi' property with version 3.x or 3.x.x" + ) + ] + ) + @staticmethod def validate_is_dict(obj, exception_message): """ diff --git a/tests/model/api/test_api_generator.py b/tests/model/api/test_api_generator.py index 391b3606f..c020085cf 100644 --- a/tests/model/api/test_api_generator.py +++ b/tests/model/api/test_api_generator.py @@ -18,7 +18,7 @@ def test_construct_usage_plan_with_invalid_usage_plan_type(self, invalid_usage_p Mock(), Mock(), Mock(), - Mock(), + {"paths": {}, "openapi": "3.0.1"}, Mock(), Mock(), Mock(), @@ -39,7 +39,7 @@ def test_construct_usage_plan_with_invalid_usage_plan_fields(self, AuthPropertie Mock(), Mock(), Mock(), - Mock(), + {"paths": {}, "openapi": "3.0.1"}, Mock(), Mock(), Mock(), diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index 346754369..d5a6e1ada 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -18,7 +18,7 @@ class TestSwaggerEditor_init(TestCase): def test_must_raise_on_invalid_swagger(self): invalid_swagger = {"paths": {}} # Missing "Swagger" keyword - with self.assertRaises(ValueError): + with self.assertRaises(InvalidDocumentException): SwaggerEditor(invalid_swagger) def test_must_succeed_on_valid_swagger(self): @@ -32,13 +32,13 @@ def test_must_succeed_on_valid_swagger(self): def test_must_fail_on_invalid_openapi_version(self): invalid_swagger = {"openapi": "2.3.0", "paths": {"/foo": {}, "/bar": {}}} - with self.assertRaises(ValueError): + with self.assertRaises(InvalidDocumentException): SwaggerEditor(invalid_swagger) def test_must_fail_on_invalid_openapi_version_2(self): invalid_swagger = {"openapi": "3.1.1.1", "paths": {"/foo": {}, "/bar": {}}} - with self.assertRaises(ValueError): + with self.assertRaises(InvalidDocumentException): SwaggerEditor(invalid_swagger) def test_must_succeed_on_valid_openapi3(self): @@ -53,7 +53,7 @@ def test_must_succeed_on_valid_openapi3(self): def test_must_fail_with_bad_values_for_path(self, invalid_path_item): invalid_swagger = {"openapi": "3.1.1.1", "paths": {"/foo": {}, "/bad": invalid_path_item}} - with self.assertRaises(ValueError): + with self.assertRaises(InvalidDocumentException): SwaggerEditor(invalid_swagger) diff --git a/tests/translator/input/api_with_resource_refs.yaml b/tests/translator/input/api_with_resource_refs.yaml index e84845cbb..13a562909 100644 --- a/tests/translator/input/api_with_resource_refs.yaml +++ b/tests/translator/input/api_with_resource_refs.yaml @@ -6,8 +6,8 @@ Resources: Properties: StageName: foo DefinitionBody: - "this": "is" - "a": "swagger" + paths: {} + openapi: "3.0.1" MyFunction: Type: "AWS::Serverless::Function" diff --git a/tests/translator/input/error_api_definition_body_invalid_openapi_version.yaml b/tests/translator/input/error_api_definition_body_invalid_openapi_version.yaml new file mode 100644 index 000000000..c1f8ab2da --- /dev/null +++ b/tests/translator/input/error_api_definition_body_invalid_openapi_version.yaml @@ -0,0 +1,13 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: + info: + version: '1.0' + title: 'title' + paths: + "/some/path": {} + "/other": {} + openapi: '2.0' \ No newline at end of file diff --git a/tests/translator/input/error_api_definition_body_invalid_paths.yaml b/tests/translator/input/error_api_definition_body_invalid_paths.yaml new file mode 100644 index 000000000..40995b908 --- /dev/null +++ b/tests/translator/input/error_api_definition_body_invalid_paths.yaml @@ -0,0 +1,11 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: + info: + version: '1.0' + title: 'title' + openapi: 3.0.1 + paths: 'invalid' \ No newline at end of file diff --git a/tests/translator/input/error_api_definition_body_missing_openapi_or_swagger.yaml b/tests/translator/input/error_api_definition_body_missing_openapi_or_swagger.yaml new file mode 100644 index 000000000..38ca085de --- /dev/null +++ b/tests/translator/input/error_api_definition_body_missing_openapi_or_swagger.yaml @@ -0,0 +1,12 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: + info: + version: '1.0' + title: 'title' + paths: + "/some/path": {} + "/other": {} \ No newline at end of file diff --git a/tests/translator/input/error_api_definition_body_missing_paths.yaml b/tests/translator/input/error_api_definition_body_missing_paths.yaml new file mode 100644 index 000000000..15de080b5 --- /dev/null +++ b/tests/translator/input/error_api_definition_body_missing_paths.yaml @@ -0,0 +1,10 @@ +Resources: + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: + info: + version: '1.0' + title: 'title' + openapi: 3.0.1 \ No newline at end of file diff --git a/tests/translator/input/error_api_invalid_auth.yaml b/tests/translator/input/error_api_invalid_auth.yaml index 60d2b3f9d..92d8c25cc 100644 --- a/tests/translator/input/error_api_invalid_auth.yaml +++ b/tests/translator/input/error_api_invalid_auth.yaml @@ -121,14 +121,6 @@ Resources: Auth: MyBad: Foo - AuthWithInvalidDefinitionBodyApi: - Type: AWS::Serverless::Api - Properties: - StageName: Prod - DefinitionBody: { invalid: true } - Auth: - DefaultAuthorizer: Foo - AuthWithMissingDefaultAuthorizerApi: Type: AWS::Serverless::Api Properties: diff --git a/tests/translator/input/error_api_invalid_definitionuri.yaml b/tests/translator/input/error_api_invalid_definitionuri.yaml index 371b3872a..90e9b09ad 100644 --- a/tests/translator/input/error_api_invalid_definitionuri.yaml +++ b/tests/translator/input/error_api_invalid_definitionuri.yaml @@ -18,5 +18,5 @@ Resources: StageName: Prod DefinitionUri: s3://foo/bar DefinitionBody: - a: b - c: d + paths: {} + openapi: "3.0.1" diff --git a/tests/translator/input/error_api_invalid_request_model.yaml b/tests/translator/input/error_api_invalid_request_model.yaml index 45616dffa..f0679d456 100644 --- a/tests/translator/input/error_api_invalid_request_model.yaml +++ b/tests/translator/input/error_api_invalid_request_model.yaml @@ -68,18 +68,6 @@ Resources: username: type: string - ModelsWithInvalidDefinitionBodyApi: - Type: AWS::Serverless::Api - Properties: - StageName: Prod - DefinitionBody: { invalid: true } - Models: - User: - type: object - properties: - username: - type: string - ModelIsNotString: Type: AWS::Serverless::Function Properties: diff --git a/tests/translator/input/explicit_api.yaml b/tests/translator/input/explicit_api.yaml index a2057640c..3fb5534f2 100644 --- a/tests/translator/input/explicit_api.yaml +++ b/tests/translator/input/explicit_api.yaml @@ -45,5 +45,5 @@ Resources: StageName: Ref: MyStageName DefinitionBody: - "this": "is" - "a": "inline swagger" + paths: {} + swagger: "2.0" diff --git a/tests/translator/input/explicit_api_openapi_3.yaml b/tests/translator/input/explicit_api_openapi_3.yaml index a6f00e5e0..f427521e7 100644 --- a/tests/translator/input/explicit_api_openapi_3.yaml +++ b/tests/translator/input/explicit_api_openapi_3.yaml @@ -46,5 +46,5 @@ Resources: Ref: MyStageName OpenApiVersion: '3.0' DefinitionBody: - "this": "is" - "a": "inline swagger" + paths: {} + openapi: "3.0" diff --git a/tests/translator/output/api_with_resource_refs.json b/tests/translator/output/api_with_resource_refs.json index e32db402a..fc171a8b2 100644 --- a/tests/translator/output/api_with_resource_refs.json +++ b/tests/translator/output/api_with_resource_refs.json @@ -99,15 +99,15 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "swagger" + "openapi": "3.0.1", + "paths": {} } } }, - "MyApiDeployment359f256a3b": { + "MyApiDeploymentd7fcdf1086": { "Type": "AWS::ApiGateway::Deployment", "Properties": { - "Description": "RestApi deployment id: 359f256a3b3ff2e1102e335a4d603f02df9b4988", + "Description": "RestApi deployment id: d7fcdf1086262180dfe8b7ad5a04cab235490858", "RestApiId": { "Ref": "MyApi" }, @@ -118,7 +118,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "MyApiDeployment359f256a3b" + "Ref": "MyApiDeploymentd7fcdf1086" }, "RestApiId": { "Ref": "MyApi" @@ -202,7 +202,7 @@ }, "ExplicitApiDeployment": { "Value": { - "Ref": "MyApiDeployment359f256a3b" + "Ref": "MyApiDeploymentd7fcdf1086" } }, "ExplicitApiStage": { diff --git a/tests/translator/output/aws-cn/api_with_resource_refs.json b/tests/translator/output/aws-cn/api_with_resource_refs.json index 42444227a..2373bc45b 100644 --- a/tests/translator/output/aws-cn/api_with_resource_refs.json +++ b/tests/translator/output/aws-cn/api_with_resource_refs.json @@ -99,8 +99,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "swagger" + "openapi": "3.0.1", + "paths": {} }, "Parameters": { "endpointConfigurationTypes": "REGIONAL" @@ -112,10 +112,10 @@ } } }, - "MyApiDeployment359f256a3b": { + "MyApiDeploymentd7fcdf1086": { "Type": "AWS::ApiGateway::Deployment", "Properties": { - "Description": "RestApi deployment id: 359f256a3b3ff2e1102e335a4d603f02df9b4988", + "Description": "RestApi deployment id: d7fcdf1086262180dfe8b7ad5a04cab235490858", "RestApiId": { "Ref": "MyApi" }, @@ -126,7 +126,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "MyApiDeployment359f256a3b" + "Ref": "MyApiDeploymentd7fcdf1086" }, "RestApiId": { "Ref": "MyApi" @@ -218,7 +218,7 @@ }, "ExplicitApiDeployment": { "Value": { - "Ref": "MyApiDeployment359f256a3b" + "Ref": "MyApiDeploymentd7fcdf1086" } }, "ExplicitApiStage": { diff --git a/tests/translator/output/aws-cn/explicit_api.json b/tests/translator/output/aws-cn/explicit_api.json index 81fb57d35..2bb41a25c 100644 --- a/tests/translator/output/aws-cn/explicit_api.json +++ b/tests/translator/output/aws-cn/explicit_api.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment09cda3d97b" + "Ref": "ApiWithInlineSwaggerDeployment4fc299d023" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -93,8 +93,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "inline swagger" + "swagger": "2.0", + "paths": {} }, "EndpointConfiguration": { "Types": [ @@ -165,13 +165,13 @@ } } }, - "ApiWithInlineSwaggerDeployment09cda3d97b": { + "ApiWithInlineSwaggerDeployment4fc299d023": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 09cda3d97b008bed7bd4ebb1b5304ed622492941", + "Description": "RestApi deployment id: 4fc299d0231d133ea66a1aa4be7d5bcd64d5f900", "StageName": "Stage" } } diff --git a/tests/translator/output/aws-cn/explicit_api_openapi_3.json b/tests/translator/output/aws-cn/explicit_api_openapi_3.json index 38e863d3a..107dc6788 100644 --- a/tests/translator/output/aws-cn/explicit_api_openapi_3.json +++ b/tests/translator/output/aws-cn/explicit_api_openapi_3.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment74abcb3a5b" + "Ref": "ApiWithInlineSwaggerDeployment532ddc6618" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -57,13 +57,13 @@ "StageName": "Stage" } }, - "ApiWithInlineSwaggerDeployment74abcb3a5b": { + "ApiWithInlineSwaggerDeployment532ddc6618": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 74abcb3a5bbe7ad58dfc543740af3be156736130" + "Description": "RestApi deployment id: 532ddc6618dbe66a96a30ed7082f375de8933a02" } }, "GetHtmlFunctionRole": { @@ -102,8 +102,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "inline swagger" + "openapi": "3.0", + "paths": {} }, "EndpointConfiguration": { "Types": [ diff --git a/tests/translator/output/aws-us-gov/api_with_resource_refs.json b/tests/translator/output/aws-us-gov/api_with_resource_refs.json index 488db17e9..357b91be2 100644 --- a/tests/translator/output/aws-us-gov/api_with_resource_refs.json +++ b/tests/translator/output/aws-us-gov/api_with_resource_refs.json @@ -99,8 +99,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "swagger" + "openapi": "3.0.1", + "paths": {} }, "Parameters": { "endpointConfigurationTypes": "REGIONAL" @@ -112,10 +112,10 @@ } } }, - "MyApiDeployment359f256a3b": { + "MyApiDeploymentd7fcdf1086": { "Type": "AWS::ApiGateway::Deployment", "Properties": { - "Description": "RestApi deployment id: 359f256a3b3ff2e1102e335a4d603f02df9b4988", + "Description": "RestApi deployment id: d7fcdf1086262180dfe8b7ad5a04cab235490858", "RestApiId": { "Ref": "MyApi" }, @@ -126,7 +126,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "MyApiDeployment359f256a3b" + "Ref": "MyApiDeploymentd7fcdf1086" }, "RestApiId": { "Ref": "MyApi" @@ -218,7 +218,7 @@ }, "ExplicitApiDeployment": { "Value": { - "Ref": "MyApiDeployment359f256a3b" + "Ref": "MyApiDeploymentd7fcdf1086" } }, "ExplicitApiStage": { diff --git a/tests/translator/output/aws-us-gov/explicit_api.json b/tests/translator/output/aws-us-gov/explicit_api.json index 5c2d3ec05..0e367a86d 100644 --- a/tests/translator/output/aws-us-gov/explicit_api.json +++ b/tests/translator/output/aws-us-gov/explicit_api.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment09cda3d97b" + "Ref": "ApiWithInlineSwaggerDeployment4fc299d023" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -93,8 +93,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "inline swagger" + "swagger": "2.0", + "paths": {} }, "EndpointConfiguration": { "Types": [ @@ -165,13 +165,13 @@ } } }, - "ApiWithInlineSwaggerDeployment09cda3d97b": { + "ApiWithInlineSwaggerDeployment4fc299d023": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 09cda3d97b008bed7bd4ebb1b5304ed622492941", + "Description": "RestApi deployment id: 4fc299d0231d133ea66a1aa4be7d5bcd64d5f900", "StageName": "Stage" } } diff --git a/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json b/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json index 49592d0b2..60f842de4 100644 --- a/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json +++ b/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment74abcb3a5b" + "Ref": "ApiWithInlineSwaggerDeployment532ddc6618" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -57,13 +57,13 @@ "StageName": "Stage" } }, - "ApiWithInlineSwaggerDeployment74abcb3a5b": { + "ApiWithInlineSwaggerDeployment532ddc6618": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 74abcb3a5bbe7ad58dfc543740af3be156736130" + "Description": "RestApi deployment id: 532ddc6618dbe66a96a30ed7082f375de8933a02" } }, "GetHtmlFunctionRole": { @@ -102,8 +102,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "inline swagger" + "openapi": "3.0", + "paths": {} }, "EndpointConfiguration": { "Types": [ diff --git a/tests/translator/output/error_api_definition_body_invalid_openapi_version.json b/tests/translator/output/error_api_definition_body_invalid_openapi_version.json new file mode 100644 index 000000000..72704bd99 --- /dev/null +++ b/tests/translator/output/error_api_definition_body_invalid_openapi_version.json @@ -0,0 +1 @@ +{"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. DefinitionBody must have either: (1) a 'swagger' property or (2) an 'openapi' property with version 3.x or 3.x.x"} \ No newline at end of file diff --git a/tests/translator/output/error_api_definition_body_invalid_paths.json b/tests/translator/output/error_api_definition_body_invalid_paths.json new file mode 100644 index 000000000..e0180529f --- /dev/null +++ b/tests/translator/output/error_api_definition_body_invalid_paths.json @@ -0,0 +1 @@ +{"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. The 'paths' property of DefinitionBody must be present and be a dictionary."} \ No newline at end of file diff --git a/tests/translator/output/error_api_definition_body_missing_openapi_or_swagger.json b/tests/translator/output/error_api_definition_body_missing_openapi_or_swagger.json new file mode 100644 index 000000000..72704bd99 --- /dev/null +++ b/tests/translator/output/error_api_definition_body_missing_openapi_or_swagger.json @@ -0,0 +1 @@ +{"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. DefinitionBody must have either: (1) a 'swagger' property or (2) an 'openapi' property with version 3.x or 3.x.x"} \ No newline at end of file diff --git a/tests/translator/output/error_api_definition_body_missing_paths.json b/tests/translator/output/error_api_definition_body_missing_paths.json new file mode 100644 index 000000000..e0180529f --- /dev/null +++ b/tests/translator/output/error_api_definition_body_missing_paths.json @@ -0,0 +1 @@ +{"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. The 'paths' property of DefinitionBody must be present and be a dictionary."} \ No newline at end of file diff --git a/tests/translator/output/error_api_invalid_auth.json b/tests/translator/output/error_api_invalid_auth.json index 323721f3b..06f59ee18 100644 --- a/tests/translator/output/error_api_invalid_auth.json +++ b/tests/translator/output/error_api_invalid_auth.json @@ -1,3 +1,3 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 16. Resource with id [AuthNotDictApi] is invalid. Type of property 'Auth' is invalid. Resource with id [AuthWithAdditionalPropertyApi] is invalid. Invalid value for 'Auth' property Resource with id [AuthWithDefinitionUriApi] is invalid. Auth works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [AuthWithInvalidDefinitionBodyApi] is invalid. Unable to add Auth configuration because 'DefinitionBody' does not contain a valid Swagger definition. Resource with id [AuthWithMissingDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because 'NotThere' was not defined in 'Authorizers'. Resource with id [AuthorizerNotDict] is invalid. Authorizer MyCognitoAuthorizer must be a dictionary. Resource with id [AuthorizersNotDictApi] is invalid. Authorizers must be a dictionary. Resource with id [InvalidFunctionPayloadTypeApi] is invalid. MyLambdaAuthorizer Authorizer has invalid 'FunctionPayloadType': INVALID. Resource with id [MissingAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [UnspecifiedAuthorizer] on API method [get] for path [/] because it wasn't defined in the API's Authorizers. Resource with id [NoApiAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthorizersFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoDefaultAuthorizerWithNoneFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer on API method [get] for path [/] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [NoIdentityOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NoIdentitySourceOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NonStringDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because intrinsic functions are not supported for this field." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 15. Resource with id [AuthNotDictApi] is invalid. Type of property 'Auth' is invalid. Resource with id [AuthWithAdditionalPropertyApi] is invalid. Invalid value for 'Auth' property Resource with id [AuthWithDefinitionUriApi] is invalid. Auth works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [AuthWithMissingDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because 'NotThere' was not defined in 'Authorizers'. Resource with id [AuthorizerNotDict] is invalid. Authorizer MyCognitoAuthorizer must be a dictionary. Resource with id [AuthorizersNotDictApi] is invalid. Authorizers must be a dictionary. Resource with id [InvalidFunctionPayloadTypeApi] is invalid. MyLambdaAuthorizer Authorizer has invalid 'FunctionPayloadType': INVALID. Resource with id [MissingAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [UnspecifiedAuthorizer] on API method [get] for path [/] because it wasn't defined in the API's Authorizers. Resource with id [NoApiAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthorizersFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoDefaultAuthorizerWithNoneFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer on API method [get] for path [/] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [NoIdentityOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NoIdentitySourceOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NonStringDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because intrinsic functions are not supported for this field." } diff --git a/tests/translator/output/error_api_invalid_request_model.json b/tests/translator/output/error_api_invalid_request_model.json index e015f1b5e..7c3422cac 100644 --- a/tests/translator/output/error_api_invalid_request_model.json +++ b/tests/translator/output/error_api_invalid_request_model.json @@ -1,3 +1,3 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [MissingModelFunction] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [UnspecifiedModel] on API method [get] for path [/] because it wasn't defined in the API's Models. Resource with id [ModelIsNotString] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [['NotString']] on API method [get] for path [/] because the related API does not contain valid Models. Resource with id [ModelsNotDictApi] is invalid. Invalid value for 'Models' property Resource with id [ModelsWithDefinitionUrlApi] is invalid. Models works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [ModelsWithInvalidDefinitionBodyApi] is invalid. Unable to add Models definitions because 'DefinitionBody' does not contain a valid Swagger definition. Resource with id [NoModelFunction] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [User] on API method [get] for path [/] because the related API does not define any Models." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 5. Resource with id [MissingModelFunction] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [UnspecifiedModel] on API method [get] for path [/] because it wasn't defined in the API's Models. Resource with id [ModelIsNotString] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [['NotString']] on API method [get] for path [/] because the related API does not contain valid Models. Resource with id [ModelsNotDictApi] is invalid. Invalid value for 'Models' property Resource with id [ModelsWithDefinitionUrlApi] is invalid. Models works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [NoModelFunction] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [User] on API method [get] for path [/] because the related API does not define any Models." } diff --git a/tests/translator/output/explicit_api.json b/tests/translator/output/explicit_api.json index cfc5f2e0f..6b64fca3c 100644 --- a/tests/translator/output/explicit_api.json +++ b/tests/translator/output/explicit_api.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment09cda3d97b" + "Ref": "ApiWithInlineSwaggerDeployment4fc299d023" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -93,8 +93,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "inline swagger" + "swagger": "2.0", + "paths": {} } } }, @@ -149,13 +149,13 @@ } } }, - "ApiWithInlineSwaggerDeployment09cda3d97b": { + "ApiWithInlineSwaggerDeployment4fc299d023": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 09cda3d97b008bed7bd4ebb1b5304ed622492941", + "Description": "RestApi deployment id: 4fc299d0231d133ea66a1aa4be7d5bcd64d5f900", "StageName": "Stage" } } diff --git a/tests/translator/output/explicit_api_openapi_3.json b/tests/translator/output/explicit_api_openapi_3.json index 4397a036c..cdec27f9f 100644 --- a/tests/translator/output/explicit_api_openapi_3.json +++ b/tests/translator/output/explicit_api_openapi_3.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment74abcb3a5b" + "Ref": "ApiWithInlineSwaggerDeployment532ddc6618" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -57,13 +57,13 @@ "StageName": "Stage" } }, - "ApiWithInlineSwaggerDeployment74abcb3a5b": { + "ApiWithInlineSwaggerDeployment532ddc6618": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 74abcb3a5bbe7ad58dfc543740af3be156736130" + "Description": "RestApi deployment id: 532ddc6618dbe66a96a30ed7082f375de8933a02" } }, "GetHtmlFunctionRole": { @@ -102,8 +102,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "this": "is", - "a": "inline swagger" + "openapi": "3.0", + "paths": {} } } }, diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 4ea9aaa51..f77ffbccc 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -752,7 +752,8 @@ def test_swagger_body_sha_gets_recomputed(): "StageName": "Prod", "DefinitionBody": { # Some body property will do - "a": "b" + "paths": {}, + "openapi": "3.0.1", }, }, } From db77d0cee7f24cb9995b6b05f5106ec2d65a3ec8 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Mon, 24 Jan 2022 15:21:01 -0800 Subject: [PATCH 38/59] Update PR template to add integration tests in checklist (#2306) --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1b91b346e..150fc8f02 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,10 +6,11 @@ *Checklist:* -- [ ] Add/update tests using: +- [ ] Add/update [unit tests](https://github.com/aws/serverless-application-model/blob/develop/DEVELOPMENT_GUIDE.md#unit-testing-with-multiple-python-versions) using: - [ ] Correct values - [ ] Bad/wrong values (None, empty, wrong type, length, etc.) - [ ] [Intrinsic Functions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html) +- [ ] Add/update [integration tests](https://github.com/aws/serverless-application-model/blob/develop/INTEGRATION_TESTS.md) - [ ] `make pr` passes - [ ] Update documentation - [ ] Verify transformed template deploys and application functions as expected From c575b54ff97600a324fefdc6f6b5b7f9343f0994 Mon Sep 17 00:00:00 2001 From: Andrew Glenn <29951057+andrew-glenn@users.noreply.github.com> Date: Tue, 25 Jan 2022 14:17:58 -0600 Subject: [PATCH 39/59] Conditionally exposing metadata to translated resources. (#2224) Co-authored-by: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> --- samtranslator/translator/transform.py | 9 +- samtranslator/translator/translator.py | 10 +- .../input/translate_convert_metadata.yaml | 19 ++++ tests/translator/output/s3.json | 2 +- .../output/translate_convert_metadata.json | 92 +++++++++++++++++++ .../output/translate_convert_no_metadata.json | 90 ++++++++++++++++++ tests/translator/test_translator.py | 31 +++++++ 7 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 tests/translator/input/translate_convert_metadata.yaml create mode 100644 tests/translator/output/translate_convert_metadata.json create mode 100644 tests/translator/output/translate_convert_no_metadata.json diff --git a/samtranslator/translator/transform.py b/samtranslator/translator/transform.py index 8be5e6000..6f225a449 100644 --- a/samtranslator/translator/transform.py +++ b/samtranslator/translator/transform.py @@ -3,7 +3,7 @@ from samtranslator.utils.py27hash_fix import to_py27_compatible_template, undo_mark_unicode_str_in_template -def transform(input_fragment, parameter_values, managed_policy_loader, feature_toggle=None): +def transform(input_fragment, parameter_values, managed_policy_loader, feature_toggle=None, passthrough_metadata=False): """Translates the SAM manifest provided in the and returns the translation to CloudFormation. :param dict input_fragment: the SAM template to transform @@ -15,6 +15,11 @@ def transform(input_fragment, parameter_values, managed_policy_loader, feature_t sam_parser = Parser() to_py27_compatible_template(input_fragment, parameter_values) translator = Translator(managed_policy_loader.load(), sam_parser) - transformed = translator.translate(input_fragment, parameter_values=parameter_values, feature_toggle=feature_toggle) + transformed = translator.translate( + input_fragment, + parameter_values=parameter_values, + feature_toggle=feature_toggle, + passthrough_metadata=passthrough_metadata, + ) transformed = undo_mark_unicode_str_in_template(transformed) return transformed diff --git a/samtranslator/translator/translator.py b/samtranslator/translator/translator.py index 3edc4ed14..5a4fe388f 100644 --- a/samtranslator/translator/translator.py +++ b/samtranslator/translator/translator.py @@ -49,6 +49,7 @@ def __init__(self, managed_policy_map, sam_parser, plugins=None, boto_session=No self.boto_session = boto_session self.metrics = metrics if metrics else Metrics("ServerlessTransform", DummyMetricsPublisher()) MetricsMethodWrapperSingleton.set_instance(self.metrics) + self._translated_resouce_mapping = {} if self.boto_session: ArnGenerator.BOTO_SESSION_REGION_NAME = self.boto_session.region_name @@ -80,7 +81,7 @@ def _get_function_names(self, resource_dict, intrinsics_resolver): ) return self.function_names - def translate(self, sam_template, parameter_values, feature_toggle=None): + def translate(self, sam_template, parameter_values, feature_toggle=None, passthrough_metadata=False): """Loads the SAM resources from the given SAM manifest, replaces them with their corresponding CloudFormation resources, and returns the resulting CloudFormation template. @@ -151,7 +152,12 @@ def translate(self, sam_template, parameter_values, feature_toggle=None): del template["Resources"][logical_id] for resource in translated: if verify_unique_logical_id(resource, sam_template["Resources"]): - template["Resources"].update(resource.to_dict()) + # For each generated resource, pass through existing metadata that may exist on the original SAM resource. + _r = resource.to_dict() + if resource_dict.get("Metadata") and passthrough_metadata: + if not template["Resources"].get(resource.logical_id): + _r[resource.logical_id]["Metadata"] = resource_dict["Metadata"] + template["Resources"].update(_r) else: document_errors.append( DuplicateLogicalIdException(logical_id, resource.logical_id, resource.resource_type) diff --git a/tests/translator/input/translate_convert_metadata.yaml b/tests/translator/input/translate_convert_metadata.yaml new file mode 100644 index 000000000..0043cba6c --- /dev/null +++ b/tests/translator/input/translate_convert_metadata.yaml @@ -0,0 +1,19 @@ +Resources: + ThumbnailFunction: + Type: AWS::Serverless::Function + Metadata: + Foo: Bar + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.generate_thumbails + Runtime: nodejs12.x + Events: + ImageBucket: + Type: S3 + Properties: + Bucket: + Ref: Images + Events: s3:ObjectCreated:* + + Images: + Type: AWS::S3::Bucket diff --git a/tests/translator/output/s3.json b/tests/translator/output/s3.json index 35b92f77c..da8ce55bc 100644 --- a/tests/translator/output/s3.json +++ b/tests/translator/output/s3.json @@ -20,7 +20,7 @@ } }, "ThumbnailFunctionRole": { - "Type": "AWS::IAM::Role", + "Type": "AWS::IAM::Role", "Properties": { "ManagedPolicyArns": [ "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" diff --git a/tests/translator/output/translate_convert_metadata.json b/tests/translator/output/translate_convert_metadata.json new file mode 100644 index 000000000..2827c0cdb --- /dev/null +++ b/tests/translator/output/translate_convert_metadata.json @@ -0,0 +1,92 @@ +{ + "Resources": { + "Images": { + "Type": "AWS::S3::Bucket", + "DependsOn": ["ThumbnailFunctionImageBucketPermission"], + "Properties": { + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Function": { + "Fn::GetAtt": [ + "ThumbnailFunction", + "Arn" + ] + }, + "Event": "s3:ObjectCreated:*" + } + ] + } + } + }, + "ThumbnailFunctionRole": { + "Metadata":{"Foo": "Bar"}, + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ThumbnailFunctionImageBucketPermission": { + "Metadata":{"Foo": "Bar"}, + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "FunctionName": { + "Ref": "ThumbnailFunction" + }, + "Principal": "s3.amazonaws.com" + } + }, + "ThumbnailFunction": { + "Metadata":{"Foo": "Bar"}, + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "thumbnails.zip" + }, + "Handler": "index.generate_thumbails", + "Role": { + "Fn::GetAtt": [ + "ThumbnailFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} diff --git a/tests/translator/output/translate_convert_no_metadata.json b/tests/translator/output/translate_convert_no_metadata.json new file mode 100644 index 000000000..a3188802d --- /dev/null +++ b/tests/translator/output/translate_convert_no_metadata.json @@ -0,0 +1,90 @@ +{ + "Resources": { + "Images": { + "Type": "AWS::S3::Bucket", + "DependsOn": ["ThumbnailFunctionImageBucketPermission"], + "Properties": { + "NotificationConfiguration": { + "LambdaConfigurations": [ + { + "Function": { + "Fn::GetAtt": [ + "ThumbnailFunction", + "Arn" + ] + }, + "Event": "s3:ObjectCreated:*" + } + ] + } + } + }, + "ThumbnailFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + } + } + }, + "ThumbnailFunctionImageBucketPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "FunctionName": { + "Ref": "ThumbnailFunction" + }, + "Principal": "s3.amazonaws.com" + } + }, + "ThumbnailFunction": { + "Type": "AWS::Lambda::Function", + "Metadata":{"Foo": "Bar"}, + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "thumbnails.zip" + }, + "Handler": "index.generate_thumbails", + "Role": { + "Fn::GetAtt": [ + "ThumbnailFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ] + } + } + } +} diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index f77ffbccc..afb2eb47e 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -929,6 +929,37 @@ def test_throws_when_resources_not_all_dicts(self): translator = Translator({}, sam_parser) translator.translate(template, {}) + @patch("boto3.session.Session.region_name", "ap-southeast-1") + @patch("botocore.client.ClientEndpointBridge._check_default_region", mock_get_region) + def test_validate_translated_no_metadata(self): + with open(os.path.join(INPUT_FOLDER, "translate_convert_metadata.yaml"), "r") as f: + template = yaml_parse(f.read()) + with open(os.path.join(OUTPUT_FOLDER, "translate_convert_no_metadata.json"), "r") as f: + expected = json.loads(f.read()) + + mock_policy_loader = get_policy_mock() + + sam_parser = Parser() + translator = Translator(mock_policy_loader, sam_parser) + actual = translator.translate(template, {}) + self.assertEqual(expected, actual) + + @patch("boto3.session.Session.region_name", "ap-southeast-1") + @patch("botocore.client.ClientEndpointBridge._check_default_region", mock_get_region) + def test_validate_translated_metadata(self): + self.maxDiff = None + with open(os.path.join(INPUT_FOLDER, "translate_convert_metadata.yaml"), "r") as f: + template = yaml_parse(f.read()) + with open(os.path.join(OUTPUT_FOLDER, "translate_convert_metadata.json"), "r") as f: + expected = json.loads(f.read()) + + mock_policy_loader = get_policy_mock() + + sam_parser = Parser() + translator = Translator(mock_policy_loader, sam_parser) + actual = translator.translate(template, {}, passthrough_metadata=True) + self.assertEqual(expected, actual) + class TestPluginsUsage(TestCase): # Tests if plugins are properly injected into the translator From f292456d2ba8375e71f283f3185cd3ad9aa083c2 Mon Sep 17 00:00:00 2001 From: Raymond Wang <14915548+wchengru@users.noreply.github.com> Date: Mon, 31 Jan 2022 15:27:58 -0800 Subject: [PATCH 40/59] Improve exception processing for Route53 with invalid type (#2284) * Improve exception processing for Route53 with invalid type * black reformat * code refactor --- samtranslator/model/api/api_generator.py | 6 ++++ ...h_custom_domains_route53_invalid_type.yaml | 33 +++++++++++++++++++ ...h_custom_domains_route53_invalid_type.json | 8 +++++ 3 files changed, 47 insertions(+) create mode 100644 tests/translator/input/error_api_with_custom_domains_route53_invalid_type.yaml create mode 100644 tests/translator/output/error_api_with_custom_domains_route53_invalid_type.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 212092696..73e4b30dc 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -513,6 +513,12 @@ def _construct_api_domain(self, rest_api): record_set_group = None if self.domain.get("Route53") is not None: route53 = self.domain.get("Route53") + if not isinstance(route53, dict): + raise InvalidResourceException( + self.logical_id, + "Invalid property type '{}' for Route53. " + "Expected a map defines an Amazon Route 53 configuration'.".format(type(route53).__name__), + ) if route53.get("HostedZoneId") is None and route53.get("HostedZoneName") is None: raise InvalidResourceException( self.logical_id, diff --git a/tests/translator/input/error_api_with_custom_domains_route53_invalid_type.yaml b/tests/translator/input/error_api_with_custom_domains_route53_invalid_type.yaml new file mode 100644 index 000000000..5e4b6b004 --- /dev/null +++ b/tests/translator/input/error_api_with_custom_domains_route53_invalid_type.yaml @@ -0,0 +1,33 @@ +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (event) => { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + return response; + }; + Handler: index.handler + Runtime: nodejs12.x + Events: + Api: + Type: Api + Properties: + RestApiId: !Ref MyApi + Method: Put + Path: /get + + MyApi: + Type: AWS::Serverless::Api + Properties: + OpenApiVersion: 3.0.1 + StageName: Prod + Domain: + DomainName: 'api-example.com' + CertificateArn: 'my-api-cert-arn' + EndpointConfiguration: 'EDGE' + BasePath: [ "/get"] + Route53: 'InvalidString' diff --git a/tests/translator/output/error_api_with_custom_domains_route53_invalid_type.json b/tests/translator/output/error_api_with_custom_domains_route53_invalid_type.json new file mode 100644 index 000000000..9df5d929e --- /dev/null +++ b/tests/translator/output/error_api_with_custom_domains_route53_invalid_type.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [MyApi] is invalid. Invalid property type 'Py27UniStr' for Route53. Expected a map defines an Amazon Route 53 configuration'." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyApi] is invalid. Invalid property type 'Py27UniStr' for Route53. Expected a map defines an Amazon Route 53 configuration'." +} From 521abe3d11472225fe4eb6f48a1079c8b0ec55cf Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Tue, 1 Feb 2022 09:26:56 -0800 Subject: [PATCH 41/59] Check S3 lambda configuration type is list (#2310) --- samtranslator/model/eventsources/push.py | 3 +++ ..._s3_lambda_configuration_invalid_type.yaml | 24 +++++++++++++++++++ ..._s3_lambda_configuration_invalid_type.json | 8 +++++++ 3 files changed, 35 insertions(+) create mode 100644 tests/translator/input/error_s3_lambda_configuration_invalid_type.yaml create mode 100644 tests/translator/output/error_s3_lambda_configuration_invalid_type.json diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 20f862707..0ab7afff5 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -403,6 +403,9 @@ def _inject_notification_configuration(self, function, bucket): lambda_notifications = [] notification_config["LambdaConfigurations"] = lambda_notifications + if not isinstance(lambda_notifications, list): + raise InvalidResourceException(self.logical_id, "Invalid type for LambdaConfigurations. Must be a list.") + for event_mapping in event_mappings: if event_mapping not in lambda_notifications: lambda_notifications.append(event_mapping) diff --git a/tests/translator/input/error_s3_lambda_configuration_invalid_type.yaml b/tests/translator/input/error_s3_lambda_configuration_invalid_type.yaml new file mode 100644 index 000000000..8cbefb041 --- /dev/null +++ b/tests/translator/input/error_s3_lambda_configuration_invalid_type.yaml @@ -0,0 +1,24 @@ +Resources: + AppFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: app.lambdaHandler + Runtime: nodejs14.x + Architectures: + - x86_64 + Events: + S3Event: + Type: S3 + Properties: + Bucket: + Ref: RandomBucket + Events: s3:ObjectCreated:* + + RandomBucket: + Type: AWS::S3::Bucket + Properties: + NotificationConfiguration: + LambdaConfigurations: + Event: s3:ObjectCreated:Put + Function: arn:aws:iam::... \ No newline at end of file diff --git a/tests/translator/output/error_s3_lambda_configuration_invalid_type.json b/tests/translator/output/error_s3_lambda_configuration_invalid_type.json new file mode 100644 index 000000000..8cab9d0b8 --- /dev/null +++ b/tests/translator/output/error_s3_lambda_configuration_invalid_type.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [AppFunctionS3Event] is invalid. Invalid type for LambdaConfigurations. Must be a list." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [AppFunctionS3Event] is invalid. Invalid type for LambdaConfigurations. Must be a list." +} \ No newline at end of file From b1b3a9c17b2d3559b902f9fe38e42233e202538a Mon Sep 17 00:00:00 2001 From: Jacob Fuss <32497805+jfuss@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:13:04 -0600 Subject: [PATCH 42/59] tests: add test for DefaultAuthorizers not a string in API (#2313) In issue #1245, we had a cases were SAM would fail due to improper validation. In updating #1286, I noticed this was patched in #1774 but we only added tests for AWS::Serverless::StateMachine. This commit adds an additional test to cover AWS::Serverless::Api. Co-authored-by: Jacob Fuss --- ...lt_authorizer_should_be_string_in_api.yaml | 30 +++++++++++++++++++ ...lt_authorizer_should_be_string_in_api.json | 3 ++ 2 files changed, 33 insertions(+) create mode 100644 tests/translator/input/error_default_authorizer_should_be_string_in_api.yaml create mode 100644 tests/translator/output/error_default_authorizer_should_be_string_in_api.json diff --git a/tests/translator/input/error_default_authorizer_should_be_string_in_api.yaml b/tests/translator/input/error_default_authorizer_should_be_string_in_api.yaml new file mode 100644 index 000000000..fca2ebb17 --- /dev/null +++ b/tests/translator/input/error_default_authorizer_should_be_string_in_api.yaml @@ -0,0 +1,30 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: "AWS::Serverless-2016-10-31" + +Resources: + MyApi: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: !Ref MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: arn:aws:1 + Identity: + Header: MyAuthorizationHeader + ValidationExpression: myauthvalidationexpression + + MyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/thumbnails.zip + Handler: index.handler + Runtime: nodejs8.10 + Events: + Api: + Type: Api + Properties: + RestApiId: !Ref MyApi + Path: / + Method: get \ No newline at end of file diff --git a/tests/translator/output/error_default_authorizer_should_be_string_in_api.json b/tests/translator/output/error_default_authorizer_should_be_string_in_api.json new file mode 100644 index 000000000..b3394c79b --- /dev/null +++ b/tests/translator/output/error_default_authorizer_should_be_string_in_api.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyApi] is invalid. DefaultAuthorizer is not a string." +} \ No newline at end of file From f37ba41ffd89dc11a7efda7262141bb0e54522ed Mon Sep 17 00:00:00 2001 From: Raymond Wang <14915548+wchengru@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:27:52 -0800 Subject: [PATCH 43/59] fix(apigw): ValueError is not caught by int() (#2305) --- samtranslator/model/apigateway.py | 2 +- tests/validator/input/api/error_auth_cognito.yaml | 4 ++++ tests/validator/output/api/error_auth_cognito.json | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 7dc400b92..a987e5a99 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -285,7 +285,7 @@ def _is_missing_identity_source(self, identity): try: ttl_int = int(ttl) # this will catch if ttl is None and not convertable to an int - except TypeError: + except (TypeError, ValueError): # previous behavior before trying to read ttl return required_properties_missing diff --git a/tests/validator/input/api/error_auth_cognito.yaml b/tests/validator/input/api/error_auth_cognito.yaml index bd957ec2c..6e3ea41a3 100644 --- a/tests/validator/input/api/error_auth_cognito.yaml +++ b/tests/validator/input/api/error_auth_cognito.yaml @@ -43,6 +43,10 @@ Resources: Identity: ReauthorizeEvery: "3" UserPoolArn: User.pool.arn + CognitoIdentityReauthorizeEveryValueError: + Identity: + ReauthorizeEvery: NotANumber + UserPoolArn: User.pool.arn CognitoIdentityReauthorizeEveryTooHigh: Identity: ReauthorizeEvery: 3601 diff --git a/tests/validator/output/api/error_auth_cognito.json b/tests/validator/output/api/error_auth_cognito.json index 76fc1c64f..6cd2814ed 100644 --- a/tests/validator/output/api/error_auth_cognito.json +++ b/tests/validator/output/api/error_auth_cognito.json @@ -9,6 +9,7 @@ "[Resources.MyApi.Properties.Auth.Authorizers.CognitoIdentityReauthorizeEveryNotInt.Identity.ReauthorizeEvery] '3' is not of type 'integer', 'intrinsic'", "[Resources.MyApi.Properties.Auth.Authorizers.CognitoIdentityReauthorizeEveryTooHigh.Identity.ReauthorizeEvery] 3601 is greater than the maximum of 3600", "[Resources.MyApi.Properties.Auth.Authorizers.CognitoIdentityReauthorizeEveryTooLow.Identity.ReauthorizeEvery] 0 is less than the minimum of 1", + "[Resources.MyApi.Properties.Auth.Authorizers.CognitoIdentityReauthorizeEveryValueError.Identity.ReauthorizeEvery] 'NotANumber' is not of type 'integer', 'intrinsic'", "[Resources.MyApi.Properties.Auth.Authorizers.CognitoIdentityValidationExpressionEmpty.Identity.ValidationExpression] Must not be empty", "[Resources.MyApi.Properties.Auth.Authorizers.CognitoIdentityValidationExpressionNotString.Identity.ValidationExpression] 3 is not of type 'string'", "[Resources.MyApi.Properties.Auth.Authorizers.CognitoUserPoolArnEmpty.UserPoolArn] Must not be empty", From 802334bfd53aaa35722c19f382d38ea2d643ba2c Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Mon, 7 Feb 2022 14:10:13 -0800 Subject: [PATCH 44/59] Add checks for authorizer event source types (#2307) --- samtranslator/model/eventsources/push.py | 36 +++++++++++-------- ...ror_api_invalid_event_authorizer_type.yaml | 28 +++++++++++++++ ...ttp_api_invalid_event_authorizer_type.yaml | 28 +++++++++++++++ ...ror_api_invalid_event_authorizer_type.json | 8 +++++ ..._api_with_invalid_auth_scopes_openapi.json | 2 +- .../output/error_http_api_invalid_auth.json | 2 +- ...ttp_api_invalid_event_authorizer_type.json | 8 +++++ 7 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 tests/translator/input/error_api_invalid_event_authorizer_type.yaml create mode 100644 tests/translator/input/error_http_api_invalid_event_authorizer_type.yaml create mode 100644 tests/translator/output/error_api_invalid_event_authorizer_type.json create mode 100644 tests/translator/output/error_http_api_invalid_event_authorizer_type.json diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 0ab7afff5..13575d17b 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -704,14 +704,9 @@ def _add_swagger_integration(self, api, function, intrinsics_resolver): ), ) - if not isinstance(method_authorizer, str): - raise InvalidEventException( - self.relative_id, - "Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}] " - "because it wasn't defined with acceptable values in the API's Authorizers.".format( - authorizer=method_authorizer, method=self.Method, path=self.Path - ), - ) + _check_valid_authorizer_types( + self.relative_id, self.Method, self.Path, method_authorizer, api_authorizers + ) if method_authorizer != "NONE" and not api_authorizers.get(method_authorizer): raise InvalidEventException( @@ -1198,13 +1193,6 @@ def _add_auth_to_openapi_integration(self, api, editor): :param editor: OpenApiEditor object that contains the OpenApi definition """ method_authorizer = self.Auth.get("Authorizer") - - if method_authorizer is not None and not isinstance(method_authorizer, str): - raise InvalidEventException( - self.relative_id, - "'Authorizer' in the 'Auth' section must be a string.", - ) - api_auth = api.get("Auth", {}) if not method_authorizer: if api_auth.get("DefaultAuthorizer"): @@ -1221,6 +1209,8 @@ def _add_auth_to_openapi_integration(self, api, editor): # Default auth should already be applied, so apply any other auth here or scope override to default api_authorizers = api_auth and api_auth.get("Authorizers") + _check_valid_authorizer_types(self.relative_id, self.Method, self.Path, method_authorizer, api_authorizers) + if method_authorizer != "NONE" and not api_authorizers: raise InvalidEventException( self.relative_id, @@ -1270,3 +1260,19 @@ def _build_apigw_integration_uri(function, partition): if function_arn.get("Fn::GetAtt") and isinstance(function_arn["Fn::GetAtt"][0], Py27UniStr): arn = Py27UniStr(arn) return Py27Dict(fnSub(arn)) + + +def _check_valid_authorizer_types(relative_id, method, path, method_authorizer, api_authorizers): + if method_authorizer == "NONE": + # If the method authorizer is "NONE" then this check + # isn't needed since DefaultAuthorizer needs to be used. + return + + if not isinstance(method_authorizer, str) or not isinstance(api_authorizers, dict): + raise InvalidEventException( + relative_id, + "Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}]. " + "The method authorizer must be a string with a corresponding dict entry in the api authorizer.".format( + authorizer=method_authorizer, method=method, path=path + ), + ) diff --git a/tests/translator/input/error_api_invalid_event_authorizer_type.yaml b/tests/translator/input/error_api_invalid_event_authorizer_type.yaml new file mode 100644 index 000000000..eda788c1f --- /dev/null +++ b/tests/translator/input/error_api_invalid_event_authorizer_type.yaml @@ -0,0 +1,28 @@ +Resources: + SignInFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: main.main + Runtime: python3.9 + Events: + MainFuncPostV1: + Type: Api + Properties: + Auth: + Authorizer: + - CognitoAuthorizer + Path: /v1/signin + RestApiId: AuthorizedApi + Method: post + AuthorizedApi: + Type: 'AWS::Serverless::Api' + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: NONE + Authorizers: + - CognitoAuthorizer: null + UserPoolArn: !GetAtt 'CognitoUserPool.Arn' + AuthorizationScopes: + - aws.cognito.signin.user.admin diff --git a/tests/translator/input/error_http_api_invalid_event_authorizer_type.yaml b/tests/translator/input/error_http_api_invalid_event_authorizer_type.yaml new file mode 100644 index 000000000..c1346f7bc --- /dev/null +++ b/tests/translator/input/error_http_api_invalid_event_authorizer_type.yaml @@ -0,0 +1,28 @@ +Resources: + SignInFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://bucket/key + Handler: main.main + Runtime: python3.9 + Events: + MainFuncPostV1: + Type: HttpApi + Properties: + Auth: + Authorizer: + - CognitoAuthorizer + Path: /v1/signin + ApiId: AuthorizedApi + Method: post + AuthorizedApi: + Type: 'AWS::Serverless::HttpApi' + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: NONE + Authorizers: + - CognitoAuthorizer: null + UserPoolArn: !GetAtt 'CognitoUserPool.Arn' + AuthorizationScopes: + - aws.cognito.signin.user.admin diff --git a/tests/translator/output/error_api_invalid_event_authorizer_type.json b/tests/translator/output/error_api_invalid_event_authorizer_type.json new file mode 100644 index 000000000..2bce26ea7 --- /dev/null +++ b/tests/translator/output/error_api_invalid_event_authorizer_type.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [AuthorizedApi] is invalid. Authorizers must be a dictionary. Resource with id [SignInFunction] is invalid. Event with id [MainFuncPostV1] is invalid. Unable to set Authorizer [['CognitoAuthorizer']] on API method [post] for path [/v1/signin]. The method authorizer must be a string with a corresponding dict entry in the api authorizer." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 2. Resource with id [AuthorizedApi] is invalid. Authorizers must be a dictionary. Resource with id [SignInFunction] is invalid. Event with id [MainFuncPostV1] is invalid. Unable to set Authorizer [['CognitoAuthorizer']] on API method [post] for path [/v1/signin]. The method authorizer must be a string with a corresponding dict entry in the api authorizer." +} diff --git a/tests/translator/output/error_api_with_invalid_auth_scopes_openapi.json b/tests/translator/output/error_api_with_invalid_auth_scopes_openapi.json index 732f3c61b..35190ce6b 100644 --- a/tests/translator/output/error_api_with_invalid_auth_scopes_openapi.json +++ b/tests/translator/output/error_api_with_invalid_auth_scopes_openapi.json @@ -4,6 +4,6 @@ "errorMessage": "Resource with id [MyApiWithCognitoAuth] is invalid. AuthorizationScopes must be a list." } ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 2. Resource with id [MyApiWithCognitoAuth] is invalid. AuthorizationScopes must be a list. Resource with id [MyFn] is invalid. Event with id [CognitoAuthorizerNotString] is invalid. Unable to set Authorizer [['NotString']] on API method [get] for path [/cognitoauthorizernotstring] because it wasn't defined with acceptable values in the API's Authorizers." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 2. Resource with id [MyApiWithCognitoAuth] is invalid. AuthorizationScopes must be a list. Resource with id [MyFn] is invalid. Event with id [CognitoAuthorizerNotString] is invalid. Unable to set Authorizer [['NotString']] on API method [get] for path [/cognitoauthorizernotstring]. The method authorizer must be a string with a corresponding dict entry in the api authorizer." } diff --git a/tests/translator/output/error_http_api_invalid_auth.json b/tests/translator/output/error_http_api_invalid_auth.json index e97089a78..45b77428a 100644 --- a/tests/translator/output/error_http_api_invalid_auth.json +++ b/tests/translator/output/error_http_api_invalid_auth.json @@ -4,5 +4,5 @@ "errorMessage": "Resource with id [Function] is invalid. Event with id [Api] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because the related API does not define any Authorizers. Resource with id [Function2] is invalid. Event with id [Api2] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because it wasn't defined in the API's Authorizers. Resource with id [Function3] is invalid. Event with id [Api3] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [Function4] is invalid. Event with id [Api4] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'AuthorizationScopes' must be a list of strings." } ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [Function] is invalid. Event with id [Api] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because the related API does not define any Authorizers. Resource with id [Function2] is invalid. Event with id [Api2] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because it wasn't defined in the API's Authorizers. Resource with id [Function3] is invalid. Event with id [Api3] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [Function4] is invalid. Event with id [Api4] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'AuthorizationScopes' must be a list of strings. Resource with id [MyApi5] is invalid. 'OpenIdConnectUrl' is no longer a supported property for authorizer 'OIDC'. Please refer to the AWS SAM documentation. Resource with id [NonStringAuthFunction] is invalid. Event with id [GetRoot] is invalid. 'Authorizer' in the 'Auth' section must be a string." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [Function] is invalid. Event with id [Api] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because the related API does not define any Authorizers. Resource with id [Function2] is invalid. Event with id [Api2] is invalid. Unable to set Authorizer [myAuth] on API method [x-amazon-apigateway-any-method] for path [$default] because it wasn't defined in the API's Authorizers. Resource with id [Function3] is invalid. Event with id [Api3] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [Function4] is invalid. Event with id [Api4] is invalid. Unable to set Authorizer on API method [x-amazon-apigateway-any-method] for path [$default] because 'AuthorizationScopes' must be a list of strings. Resource with id [MyApi5] is invalid. 'OpenIdConnectUrl' is no longer a supported property for authorizer 'OIDC'. Please refer to the AWS SAM documentation. Resource with id [NonStringAuthFunction] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [{'Ref': 'MyAuth'}] on API method [get] for path [/]. The method authorizer must be a string with a corresponding dict entry in the api authorizer." } \ No newline at end of file diff --git a/tests/translator/output/error_http_api_invalid_event_authorizer_type.json b/tests/translator/output/error_http_api_invalid_event_authorizer_type.json new file mode 100644 index 000000000..2bce26ea7 --- /dev/null +++ b/tests/translator/output/error_http_api_invalid_event_authorizer_type.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Resource with id [AuthorizedApi] is invalid. Authorizers must be a dictionary. Resource with id [SignInFunction] is invalid. Event with id [MainFuncPostV1] is invalid. Unable to set Authorizer [['CognitoAuthorizer']] on API method [post] for path [/v1/signin]. The method authorizer must be a string with a corresponding dict entry in the api authorizer." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 2. Resource with id [AuthorizedApi] is invalid. Authorizers must be a dictionary. Resource with id [SignInFunction] is invalid. Event with id [MainFuncPostV1] is invalid. Unable to set Authorizer [['CognitoAuthorizer']] on API method [post] for path [/v1/signin]. The method authorizer must be a string with a corresponding dict entry in the api authorizer." +} From 979825fdf44b5f06036996f0446b872bf52bc729 Mon Sep 17 00:00:00 2001 From: marekaiv <85357404+marekaiv@users.noreply.github.com> Date: Mon, 14 Feb 2022 13:59:36 -0800 Subject: [PATCH 45/59] Add method for determining service availability in a region (#2321) --- .../application/serverless_app_plugin.py | 2 +- samtranslator/region_configuration.py | 37 ++++++++++--------- tests/unit/test_region_configuration.py | 24 ++++++++++++ 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/samtranslator/plugins/application/serverless_app_plugin.py b/samtranslator/plugins/application/serverless_app_plugin.py index 3b6e6246c..5b1131e3b 100644 --- a/samtranslator/plugins/application/serverless_app_plugin.py +++ b/samtranslator/plugins/application/serverless_app_plugin.py @@ -111,7 +111,7 @@ def on_before_transform_template(self, template_dict): if key not in self._applications: try: - if not RegionConfiguration.is_sar_supported(): + if not RegionConfiguration.is_service_supported("serverlessrepo"): raise InvalidResourceException( logical_id, "Serverless Application Repository is not available in this region." ) diff --git a/samtranslator/region_configuration.py b/samtranslator/region_configuration.py index 73f802e1c..8656549f0 100644 --- a/samtranslator/region_configuration.py +++ b/samtranslator/region_configuration.py @@ -26,31 +26,32 @@ def is_apigw_edge_configuration_supported(cls): ] @classmethod - def is_sar_supported(cls): + def is_service_supported(cls, service, region=None): """ - SAR is not supported in some regions. + Not all services are supported in all regions. This method returns whether a given + service is supported in a given region. If no region is specified, the current region + (as identified by boto3) is used. https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/ - https://docs.aws.amazon.com/general/latest/gr/serverlessrepo.html - :return: True, if SAR is supported in current region. + :param service: service code (string used to obtain a boto3 client for the service) + :param region: region identifier (e.g., us-east-1) + :return: True, if the service is supported in the region """ session = boto3.Session() - # get the current region - region = session.region_name + if not region: + # get the current region + region = session.region_name - # need to handle when region is None so that it won't break - if region is None: - if ArnGenerator.BOTO_SESSION_REGION_NAME is not None: - region = ArnGenerator.BOTO_SESSION_REGION_NAME - else: - raise NoRegionFound("AWS Region cannot be found") + # need to handle when region is None so that it won't break + if region is None: + if ArnGenerator.BOTO_SESSION_REGION_NAME is not None: + region = ArnGenerator.BOTO_SESSION_REGION_NAME + else: + raise NoRegionFound("AWS Region cannot be found") - # boto3 get_available_regions call won't return us-gov and cn regions even if SAR is available - if region.startswith("cn") or region.startswith("us-gov"): - return True - - # get all regions where SAR are available - available_regions = session.get_available_regions("serverlessrepo") + # check if the service is available in region + partition = session.get_partition_for_region(region) + available_regions = session.get_available_regions(service, partition_name=partition) return region in available_regions diff --git a/tests/unit/test_region_configuration.py b/tests/unit/test_region_configuration.py index ac45832f4..699596e37 100644 --- a/tests/unit/test_region_configuration.py +++ b/tests/unit/test_region_configuration.py @@ -36,3 +36,27 @@ def test_when_apigw_edge_configuration_is_not_supported(self, partition): get_partition_name_patch.return_value = partition self.assertFalse(RegionConfiguration.is_apigw_edge_configuration_supported()) + + @parameterized.expand( + [ + # use ec2 as it's just about everywhere + ["ec2", "cn-north-1"], + ["ec2", "us-west-2"], + ["ec2", "us-gov-east-1"], + ["ec2", "us-isob-east-1"], + ["ec2", None], + # test SAR since SAM uses that + ["serverlessrepo", "us-east-1"], + ["serverlessrepo", "ap-southeast-2"], + ] + ) + def test_is_service_supported_positive(self, service, region): + self.assertTrue(RegionConfiguration.is_service_supported(service, region)) + + def test_is_service_supported_negative(self): + # use an unknown service name + self.assertFalse(RegionConfiguration.is_service_supported("ec1", "us-east-1")) + # use a region that does not exist + self.assertFalse(RegionConfiguration.is_service_supported("ec2", "us-east-0")) + # hard to test with a real service, since the test may start failing once that + # service is rolled out to more regions... From 059b082add22e30fe959a25d5ea5b87bf38c763f Mon Sep 17 00:00:00 2001 From: Jacob Fuss <32497805+jfuss@users.noreply.github.com> Date: Wed, 16 Feb 2022 11:27:31 -0600 Subject: [PATCH 46/59] chore: Enable pylint on project (#2326) Previously, this repo did not do any linting. The linting that is enabled is basic, as this commit disables all of what pylint was complaining about. Co-authored-by: Jacob Fuss --- .pylintrc | 123 +++++++++++++++++++++++++++++++++++++++---- Makefile | 6 ++- requirements/dev.txt | 2 +- 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6994f5712..3c55a73b0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,7 +9,7 @@ # Add files or directories to the ignore list. They should be base names, not # paths. -ignore=compat.py +ignore=compat.py, __main__.py # Pickle collected data for later comparisons. persistent=yes @@ -17,8 +17,13 @@ persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= + pylint.extensions.check_elif, # Else If Used checker + pylint.extensions.emptystring, # compare to empty string + pylint.extensions.comparetozero, # compare to 0 + pylint.extensions.docparams, # Parameter documentation checker # Use multiple processes to speed up Pylint. +# Pylint has a bug on multitread, turn it off. jobs=1 # Allow loading of arbitrary C extensions. Extensions are imported into the @@ -59,7 +64,103 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=R0201,W0613,I0021,I0020,W1618,W1619,R0902,R0903,W0231,W0611,R0913,W0703,C0330,R0204,I0011,R0904 +disable= + W0613, # Unused argument %r + W0640, # Cell variable %s defined in loop A variable used in a closure is defined in a loop + R0902, # Too many instance attributes (%s/%s) + R0903, # Too few public methods (%s/%s) + R0913, # Too many arguments (%s/%s) + W0703, # Catching too general exception %s + R0904, # Too many public methods (%s/%s) + R0914, # Too many local variables (%s/%s) + R0915, # Too many statements + C0415, # Import outside toplevel (%s) Used when an import statement is used anywhere other than the module toplevel. Move this import to the top of the file. + C0115, # missing-class-docstring + # below are docstring lint rules, in order to incrementally improve our docstring while + # without introduce too much effort at a time, we exclude most of the docstring lint rules. + # We will remove them one by one. + # W9005, # "%s" has constructor parameters documented in class and __init__ + W9006, # "%s" not documented as being raised + # W9008, # Redundant returns documentation + # W9010, # Redundant yields documentation + W9011, # Missing return documentation + W9012, # Missing return type documentation + # W9013, # Missing yield documentation + # W9014, # Missing yield type documentation + # W9015, # "%s" missing in parameter documentation + W9016, # "%s" missing in parameter type documentation + # W9017, # "%s" differing in parameter documentation + # W9018, # "%s" differing in parameter type documentation + # W9019, # "%s" useless ignored parameter documentation + # W9020, # "%s" useless ignored parameter type documentation + # Constant name style warnings. We will remove them one by one. + C0103, # Class constant name "%s" doesn't conform to UPPER_CASE naming style ('([^\\W\\da-z][^\\Wa-z]*|__.*__)$' pattern) (invalid-name) + # New recommendations, disable for now to avoid introducing behaviour changes. + R1729, # Use a generator instead '%s' (use-a-generator) + R1732, # Consider using 'with' for resource-allocating operations (consider-using-with) + # This applies to CPython only. + I1101, # Module 'math' has no 'pow' member, but source is unavailable. Consider adding this module to extension-pkg-allow-list if you want to perform analysis based on run-time introspection of living objects. (c-extension-no-member) + # Added to allow linting. Need to work on removing over time + R0801, # dupilcated code + R0401, # Cyclic import + C0411, # import ordering + W9015, # missing parameter in doc strings + W0612, # unused variable + R0205, # useless-object-inheritanc + C0301, # line to long + R0201, # no self use, method could be a function + C0114, # missing-module-docstring + W1202, # Use lazy % formatting in logging functions (logging-format-interpolation) + E1101, # No member + W0622, # Redefining built-in 'property' (redefined-builtin) + W0611, # unused imports + W0231, # super not called + W0212, # protected-access + W0201, # attribute-defined-outside-init + R1725, # Consider using Python 3 style super() without arguments (super-with-arguments) + C2001, # Avoid comparisons to zero (compare-to-zero) + R0912, # too many branches + W0235, # Useless super delegation in method '__init__' (useless-super-delegation) + C0412, # Imports from package samtranslator are not grouped (ungrouped-imports) + W0223, # abstract-method + W0107, # unnecessary-pass + W0707, # raise-missing-from + R1720, # no-else-raise + W0621, # redefined-outer-name + E0203, # access-member-before-definition + W0221, # arguments-differ + R1710, # inconsistent-return-statements + R1702, # too-many-nested-blocks + C0123, # Use isinstance() rather than type() for a typecheck. (unidiomatic-typecheck) + W0105, # String statement has no effect (pointless-string-statement) + C0206, # Consider iterating with .items() (consider-using-dict-items) + W9008, # Redundant returns documentation (redundant-returns-doc) + E0602, # undefined-variable + C0112, # empty-docstring + C0116, # missing-function-docstring + C0200, # consider-using-enumerate + R5501, # Consider using "elif" instead of "else if" (else-if-used) + W9017, # differing-param-doc + W9018, # differing-type-doc + W0511, # fixme + C0325, # superfluous-parens + R1701, # consider-merging-isinstance + C0302, # too-many-lines + C0303, # trailing-whitespace + R1721, # unnecessary-comprehension + C0121, # singleton-comparison + E1305, # too-many-format-args + R1724, # no-else-continue + E0611, # no-name-in-module + W9013, # missing-yield-doc + W9014, # missing-yield-type-doc + C0201, # consider-iterating-dictionary + W0237, # arguments-renamed + R1718, # consider-using-set-comprehension + R1723, # no-else-break + E1133, # not-an-iterable + E1135, # unsupported-membership-test + R1705, # no-else-return [REPORTS] @@ -162,18 +263,20 @@ module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ +method-rgx=[a-z_][a-z0-9_]{2,50}$ # Naming hint for method names method-name-hint=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match function or class names that do # not require a docstring. -no-docstring-rgx=.* +no-docstring-rgx=^_ # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. -docstring-min-length=-1 +# To improve our docstring without spending too much effort at a time, +# here set it to 30 and decrease it gradually in the future. +docstring-min-length=30 [FORMAT] @@ -221,16 +324,16 @@ notes=FIXME,XXX [SIMILARITIES] # Minimum lines number of a similarity. -min-similarity-lines=5 +min-similarity-lines=12 # Ignore comments when computing similarities. -ignore-comments=no +ignore-comments=yes # Ignore docstrings when computing similarities. -ignore-docstrings=no +ignore-docstrings=yes # Ignore imports when computing similarities. -ignore-imports=no +ignore-imports=yes [SPELLING] @@ -315,7 +418,7 @@ max-args=5 ignored-argument-names=_.* # Maximum number of locals for function / method body -max-locals=15 +max-locals=17 # Maximum number of return / yield for function / method body max-returns=6 diff --git a/Makefile b/Makefile index 21f96495f..14d2ca642 100755 --- a/Makefile +++ b/Makefile @@ -20,11 +20,15 @@ black: black-check: black --check setup.py samtranslator/* tests/* integration/* bin/*.py +lint: + # Linter performs static analysis to catch latent bugs + pylint --rcfile .pylintrc samtranslator + # Command to run everytime you make changes to verify everything works dev: test # Verifications to run before sending a pull request -pr: black-check init dev +pr: black-check lint init dev define HELP_MESSAGE diff --git a/requirements/dev.txt b/requirements/dev.txt index 72ee04f7b..a2dcac6e7 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ tox~=3.24 pytest-cov~=2.10.1 pytest-xdist~=2.5 pytest-env~=0.6.2 -pylint>=1.7.2,<2.0 +pylint~=2.9.0 pyyaml~=5.4 # Test requirements From 2956d26ceada0c457e4d7073edcd7e4a12047c71 Mon Sep 17 00:00:00 2001 From: jonife <79116465+jonife@users.noreply.github.com> Date: Mon, 21 Feb 2022 11:18:41 -0600 Subject: [PATCH 47/59] fix: Update validation for dead letter queue (#2324) --- samtranslator/model/sam_resources.py | 6 ++++++ ...function_with_invalid_dlq_property_type.yaml | 17 +++++++++++++++++ ...function_with_invalid_dlq_property_type.json | 8 ++++++++ 3 files changed, 31 insertions(+) create mode 100644 tests/translator/input/error_function_with_invalid_dlq_property_type.yaml create mode 100644 tests/translator/output/error_function_with_invalid_dlq_property_type.json diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 5e6c582f3..57efa747f 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -593,6 +593,12 @@ def _validate_dlq(self): "'DeadLetterQueue' requires Type and TargetArn properties to be specified.".format(valid_dlq_types), ) + if not (isinstance(self.DeadLetterQueue.get("Type"), str)): + raise InvalidResourceException( + self.logical_id, + "'DeadLetterQueue' property 'Type' should be of type str.", + ) + # Validate required Types if not self.DeadLetterQueue["Type"] in self.dead_letter_queue_policy_actions: raise InvalidResourceException( diff --git a/tests/translator/input/error_function_with_invalid_dlq_property_type.yaml b/tests/translator/input/error_function_with_invalid_dlq_property_type.yaml new file mode 100644 index 000000000..276a46bd1 --- /dev/null +++ b/tests/translator/input/error_function_with_invalid_dlq_property_type.yaml @@ -0,0 +1,17 @@ +Transform: "AWS::Serverless-2016-10-31" +Parameter: + DeadLetterQueueType: + Type: String + Default: SQS + +Resources: + MySqsDlqLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: python2.7 + CodeUri: s3://sam-demo-bucket/hello.zip + DeadLetterQueue: + Type: + Ref: DeadLetterQueueType + TargetArn: arn diff --git a/tests/translator/output/error_function_with_invalid_dlq_property_type.json b/tests/translator/output/error_function_with_invalid_dlq_property_type.json new file mode 100644 index 000000000..a901eca0d --- /dev/null +++ b/tests/translator/output/error_function_with_invalid_dlq_property_type.json @@ -0,0 +1,8 @@ +{ + "errors": [ + { + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MySqsDlqLambdaFunction] is invalid. 'DeadLetterQueue' property 'Type' should be of type str." + } + ], + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MySqsDlqLambdaFunction] is invalid. 'DeadLetterQueue' property 'Type' should be of type str." +} \ No newline at end of file From 87d2a88864487e4fda5158d67fa05c0c9bf61e25 Mon Sep 17 00:00:00 2001 From: Ruperto Torres <86501267+torresxb1@users.noreply.github.com> Date: Tue, 22 Feb 2022 15:43:55 -0800 Subject: [PATCH 48/59] fix: fix paths IF intrinsic bug (#2330) --- samtranslator/model/eventsources/push.py | 4 +- samtranslator/open_api/open_api.py | 303 ++++----- samtranslator/swagger/swagger.py | 582 +++++++++--------- tests/openapi/test_openapi.py | 21 +- tests/swagger/test_swagger.py | 32 +- .../api_http_paths_with_if_condition.yaml | 68 ++ ..._with_if_condition_no_value_else_case.yaml | 62 ++ ..._with_if_condition_no_value_then_case.yaml | 62 ++ ..._rest_paths_with_if_condition_openapi.yaml | 76 +++ ..._condition_openapi_no_value_else_case.yaml | 70 +++ ..._condition_openapi_no_value_then_case.yaml | 70 +++ ..._rest_paths_with_if_condition_swagger.yaml | 76 +++ ..._condition_swagger_no_value_else_case.yaml | 70 +++ ..._condition_swagger_no_value_then_case.yaml | 70 +++ .../api_http_paths_with_if_condition.json | 178 ++++++ ..._with_if_condition_no_value_else_case.json | 166 +++++ ..._with_if_condition_no_value_then_case.json | 166 +++++ ..._rest_paths_with_if_condition_openapi.json | 324 ++++++++++ ..._condition_openapi_no_value_else_case.json | 253 ++++++++ ..._condition_openapi_no_value_then_case.json | 253 ++++++++ ..._rest_paths_with_if_condition_swagger.json | 322 ++++++++++ ..._condition_swagger_no_value_else_case.json | 251 ++++++++ ..._condition_swagger_no_value_then_case.json | 251 ++++++++ .../api_http_paths_with_if_condition.json | 178 ++++++ ..._with_if_condition_no_value_else_case.json | 166 +++++ ..._with_if_condition_no_value_then_case.json | 166 +++++ ..._rest_paths_with_if_condition_openapi.json | 332 ++++++++++ ..._condition_openapi_no_value_else_case.json | 261 ++++++++ ..._condition_openapi_no_value_then_case.json | 261 ++++++++ ..._rest_paths_with_if_condition_swagger.json | 330 ++++++++++ ..._condition_swagger_no_value_else_case.json | 259 ++++++++ ..._condition_swagger_no_value_then_case.json | 259 ++++++++ .../api_http_paths_with_if_condition.json | 178 ++++++ ..._with_if_condition_no_value_else_case.json | 166 +++++ ..._with_if_condition_no_value_then_case.json | 166 +++++ ..._rest_paths_with_if_condition_openapi.json | 332 ++++++++++ ..._condition_openapi_no_value_else_case.json | 261 ++++++++ ..._condition_openapi_no_value_then_case.json | 261 ++++++++ ..._rest_paths_with_if_condition_swagger.json | 330 ++++++++++ ..._condition_swagger_no_value_else_case.json | 259 ++++++++ ..._condition_swagger_no_value_then_case.json | 259 ++++++++ tests/translator/test_translator.py | 9 + 42 files changed, 7692 insertions(+), 471 deletions(-) create mode 100644 tests/translator/input/api_http_paths_with_if_condition.yaml create mode 100644 tests/translator/input/api_http_paths_with_if_condition_no_value_else_case.yaml create mode 100644 tests/translator/input/api_http_paths_with_if_condition_no_value_then_case.yaml create mode 100644 tests/translator/input/api_rest_paths_with_if_condition_openapi.yaml create mode 100644 tests/translator/input/api_rest_paths_with_if_condition_openapi_no_value_else_case.yaml create mode 100644 tests/translator/input/api_rest_paths_with_if_condition_openapi_no_value_then_case.yaml create mode 100644 tests/translator/input/api_rest_paths_with_if_condition_swagger.yaml create mode 100644 tests/translator/input/api_rest_paths_with_if_condition_swagger_no_value_else_case.yaml create mode 100644 tests/translator/input/api_rest_paths_with_if_condition_swagger_no_value_then_case.yaml create mode 100644 tests/translator/output/api_http_paths_with_if_condition.json create mode 100644 tests/translator/output/api_http_paths_with_if_condition_no_value_else_case.json create mode 100644 tests/translator/output/api_http_paths_with_if_condition_no_value_then_case.json create mode 100644 tests/translator/output/api_rest_paths_with_if_condition_openapi.json create mode 100644 tests/translator/output/api_rest_paths_with_if_condition_openapi_no_value_else_case.json create mode 100644 tests/translator/output/api_rest_paths_with_if_condition_openapi_no_value_then_case.json create mode 100644 tests/translator/output/api_rest_paths_with_if_condition_swagger.json create mode 100644 tests/translator/output/api_rest_paths_with_if_condition_swagger_no_value_else_case.json create mode 100644 tests/translator/output/api_rest_paths_with_if_condition_swagger_no_value_then_case.json create mode 100644 tests/translator/output/aws-cn/api_http_paths_with_if_condition.json create mode 100644 tests/translator/output/aws-cn/api_http_paths_with_if_condition_no_value_else_case.json create mode 100644 tests/translator/output/aws-cn/api_http_paths_with_if_condition_no_value_then_case.json create mode 100644 tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi.json create mode 100644 tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi_no_value_else_case.json create mode 100644 tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi_no_value_then_case.json create mode 100644 tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger.json create mode 100644 tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger_no_value_else_case.json create mode 100644 tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger_no_value_then_case.json create mode 100644 tests/translator/output/aws-us-gov/api_http_paths_with_if_condition.json create mode 100644 tests/translator/output/aws-us-gov/api_http_paths_with_if_condition_no_value_else_case.json create mode 100644 tests/translator/output/aws-us-gov/api_http_paths_with_if_condition_no_value_then_case.json create mode 100644 tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi.json create mode 100644 tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi_no_value_else_case.json create mode 100644 tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi_no_value_then_case.json create mode 100644 tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger.json create mode 100644 tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger_no_value_else_case.json create mode 100644 tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger_no_value_then_case.json diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 13575d17b..b9a6f5f7b 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -1118,8 +1118,8 @@ def _get_permission(self, resources_to_link, stage): # If this is using the new $default path, keep path blank and add a * permission if path == OpenApiEditor._DEFAULT_PATH: path = "" - elif editor and resources_to_link.get("function").logical_id == editor.get_integration_function_logical_id( - OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD + elif editor and editor.is_integration_function_logical_id_match( + OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD, resources_to_link.get("function").logical_id ): # Case where default exists for this function, and so the permissions for that will apply here as well # This can save us several CFN resources (not duplicating permissions) diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index d67f7aac9..7c7bb5826 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -1,9 +1,7 @@ import copy import re -from samtranslator.model.intrinsics import ref -from samtranslator.model.intrinsics import make_conditional -from samtranslator.model.intrinsics import is_intrinsic +from samtranslator.model.intrinsics import ref, make_conditional, is_intrinsic, is_intrinsic_no_value from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr import json @@ -54,79 +52,87 @@ def __init__(self, doc): self.tags = self._doc.get("tags", []) self.info = self._doc.get("info", Py27Dict()) - def get_path(self, path): + def get_conditional_contents(self, item): """ - Returns the contents of a path, extracting them out of a condition if necessary - :param path: path name + Returns the contents of the given item. + If a conditional block has been used inside the item, returns a list of the content + inside the conditional (both the then and the else cases). Skips {'Ref': 'AWS::NoValue'} content. + If there's no conditional block, then returns an list with the single item in it. + + :param dict item: item from which the contents will be extracted + :return: list of item content """ - path_dict = self.paths.get(path) - if isinstance(path_dict, dict) and self._CONDITIONAL_IF in path_dict: - path_dict = path_dict[self._CONDITIONAL_IF][1] - return path_dict + contents = [item] + if isinstance(item, dict) and self._CONDITIONAL_IF in item: + contents = item[self._CONDITIONAL_IF][1:] + contents = [content for content in contents if not is_intrinsic_no_value(content)] + return contents def has_path(self, path, method=None): """ - Returns True if this OpenApi has the given path and optional method + Returns True if this Swagger has the given path and optional method + For paths with conditionals, only returns true if both items (true case, and false case) have the method. :param string path: Path name :param string method: HTTP method :return: True, if this path/method is present in the document """ - method = self._normalize_method_name(method) + if path not in self.paths: + return False - path_dict = self.get_path(path) - path_dict_exists = path_dict is not None + method = self._normalize_method_name(method) if method: - return path_dict_exists and method in path_dict - return path_dict_exists - - def get_integration_function_logical_id(self, path_name, method_name): - """ - Retrieves the function logical id in a lambda integration if it exists - If it doesn't exist, returns false + for path_item in self.get_conditional_contents(self.paths.get(path)): + if not path_item or method not in path_item: + return False + return True + + def is_integration_function_logical_id_match(self, path_name, method_name, logical_id): + """ + Returns True if the function logical id in a lambda integration matches the passed + in logical_id. + If there are conditionals (paths, methods, uri), returns True only + if they all match the passed in logical_id. False otherwise. + If the integration doesn't exist, returns False :param path_name: name of the path :param method_name: name of the method + :param logical_id: logical id to compare against """ if not self.has_integration(path_name, method_name): return False method_name = self._normalize_method_name(method_name) - # Get the path - path = self.get_path(path_name) - # Get the method contents - # We only want the first one in case there are multiple (in a conditional) - method = self.get_method_contents(path[method_name])[0] - integration = method.get(self._X_APIGW_INTEGRATION, Py27Dict()) - - # Extract the integration uri out of a conditional if necessary - uri = integration.get("uri") - if not isinstance(uri, dict): - return "" - if self._CONDITIONAL_IF in uri: - arn = uri[self._CONDITIONAL_IF][1].get("Fn::Sub") - else: - arn = uri.get("Fn::Sub", "") - - # Extract lambda integration (${LambdaName.Arn}) and split ".Arn" off from it - regex = r"([A-Za-z0-9]+\.Arn)" - matches = re.findall(regex, arn) - # Prevent IndexError when integration URI doesn't contain .Arn (e.g. a Function with - # AutoPublishAlias translates to AWS::Lambda::Alias, which make_shorthand represents - # as LogicalId instead of LogicalId.Arn). - # TODO: Consistent handling of Functions with and without AutoPublishAlias (see #1901) - if not matches: - return False - match = matches[0].split(".Arn")[0] - return match + + for method_definition in self.iter_on_method_definitions_for_path_at_method(path_name, method_name, False): + integration = method_definition.get(self._X_APIGW_INTEGRATION, Py27Dict()) + + # Extract the integration uri out of a conditional if necessary + uri = integration.get("uri") + if not isinstance(uri, dict): + return False + for uri_content in self.get_conditional_contents(uri): + arn = uri_content.get("Fn::Sub", "") + + # Extract lambda integration (${LambdaName.Arn}) and split ".Arn" off from it + regex = r"([A-Za-z0-9]+\.Arn)" + matches = re.findall(regex, arn) + # Prevent IndexError when integration URI doesn't contain .Arn (e.g. a Function with + # AutoPublishAlias translates to AWS::Lambda::Alias, which make_shorthand represents + # as LogicalId instead of LogicalId.Arn). + # TODO: Consistent handling of Functions with and without AutoPublishAlias (see #1901) + if not matches or matches[0].split(".Arn")[0] != logical_id: + return False + + return True def method_has_integration(self, method): """ Returns true if the given method contains a valid method definition. - This uses the get_method_contents function to handle conditionals. + This uses the get_conditional_contents function to handle conditionals. :param dict method: method dictionary :return: true if method has one or multiple integrations """ - for method_definition in self.get_method_contents(method): + for method_definition in self.get_conditional_contents(method): if self.method_definition_has_integration(method_definition): return True return False @@ -142,22 +148,10 @@ def method_definition_has_integration(self, method_definition): return True return False - def get_method_contents(self, method): - """ - Returns the swagger contents of the given method. This checks to see if a conditional block - has been used inside of the method, and, if so, returns the method contents that are - inside of the conditional. - - :param dict method: method dictionary - :return: list of swagger component dictionaries for the method - """ - if self._CONDITIONAL_IF in method: - return method[self._CONDITIONAL_IF][1:] - return [method] - def has_integration(self, path, method): """ - Checks if an API Gateway integration is already present at the given path/method + Checks if an API Gateway integration is already present at the given path/method. + For paths with conditionals, it only returns True if both items (true case, false case) have the integration :param string path: Path name :param string method: HTTP method @@ -165,12 +159,15 @@ def has_integration(self, path, method): """ method = self._normalize_method_name(method) - path_dict = self.get_path(path) - return ( - self.has_path(path, method) - and isinstance(path_dict[method], dict) - and self.method_has_integration(path_dict[method]) - ) # Integration present and non-empty + if not self.has_path(path, method): + return False + + for path_item in self.get_conditional_contents(self.paths.get(path)): + method_definition = path_item.get(method) + if not (isinstance(method_definition, dict) and self.method_has_integration(method_definition)): + return False + # Integration present and non-empty + return True def add_path(self, path, method=None): """ @@ -194,10 +191,8 @@ def add_path(self, path, method=None): ] ) - if self._CONDITIONAL_IF in path_dict: - path_dict = path_dict[self._CONDITIONAL_IF][1] - - path_dict.setdefault(method, Py27Dict()) + for path_item in self.get_conditional_contents(path_dict): + path_item.setdefault(method, Py27Dict()) def add_lambda_integration( self, path, method, integration_uri, method_auth_config=None, api_auth_config=None, condition=None @@ -222,23 +217,23 @@ def add_lambda_integration( if condition: integration_uri = make_conditional(condition, integration_uri) - path_dict = self.get_path(path) - # create as Py27Dict and insert key one by one to preserve input order - path_dict[method][self._X_APIGW_INTEGRATION] = Py27Dict() - path_dict[method][self._X_APIGW_INTEGRATION]["type"] = "aws_proxy" - path_dict[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" - path_dict[method][self._X_APIGW_INTEGRATION]["payloadFormatVersion"] = "2.0" - path_dict[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri + for path_item in self.get_conditional_contents(self.paths.get(path)): + # create as Py27Dict and insert key one by one to preserve input order + path_item[method][self._X_APIGW_INTEGRATION] = Py27Dict() + path_item[method][self._X_APIGW_INTEGRATION]["type"] = "aws_proxy" + path_item[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" + path_item[method][self._X_APIGW_INTEGRATION]["payloadFormatVersion"] = "2.0" + path_item[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri - if path == self._DEFAULT_PATH and method == self._X_ANY_METHOD: - path_dict[method]["isDefaultRoute"] = True + if path == self._DEFAULT_PATH and method == self._X_ANY_METHOD: + path_item[method]["isDefaultRoute"] = True - # If 'responses' key is *not* present, add it with an empty dict as value - path_dict[method].setdefault("responses", Py27Dict()) + # If 'responses' key is *not* present, add it with an empty dict as value + path_item[method].setdefault("responses", Py27Dict()) - # If a condition is present, wrap all method contents up into the condition - if condition: - path_dict[method] = make_conditional(condition, path_dict[method]) + # If a condition is present, wrap all method contents up into the condition + if condition: + path_item[method] = make_conditional(condition, path_item[method]) def make_path_conditional(self, path, condition): """ @@ -259,6 +254,46 @@ def iter_on_path(self): for path, value in self.paths.items(): yield path + def iter_on_method_definitions_for_path_at_method( + self, path_name, method_name, skip_methods_without_apigw_integration=True + ): + """ + Yields all the method definitions for the path+method combinations if path and/or method have IF conditionals. + If there are no conditionals, will just yield the single method definition at the given path and method name. + + :param path_name: path name + :param method_name: method name + :param skip_methods_without_apigw_integration: if True, skips method definitions without apigw integration + :yields dict: method definition + """ + normalized_method_name = self._normalize_method_name(method_name) + + for path_item in self.get_conditional_contents(self.paths.get(path_name)): + for method_definition in self.get_conditional_contents(path_item.get(normalized_method_name)): + if skip_methods_without_apigw_integration and not self.method_definition_has_integration( + method_definition + ): + continue + yield method_definition + + def iter_on_all_methods_for_path(self, path_name, skip_methods_without_apigw_integration=True): + """ + Yields all the (method name, method definition) tuples for the path, including those inside conditionals. + + :param path_name: path name + :param skip_methods_without_apigw_integration: if True, skips method definitions without apigw integration + :yields list of (method name, method definition) tuples + """ + for path_item in self.get_conditional_contents(self.paths.get(path_name)): + for method_name, method in path_item.items(): + for method_definition in self.get_conditional_contents(method): + if skip_methods_without_apigw_integration and not self.method_definition_has_integration( + method_definition + ): + continue + normalized_method_name = self._normalize_method_name(method_name) + yield normalized_method_name, method_definition + def add_timeout_to_method(self, api, path, method_name, timeout): """ Adds a timeout to this path/method. @@ -268,10 +303,8 @@ def add_timeout_to_method(self, api, path, method_name, timeout): :param string method_name: Method name :param int timeout: Timeout amount, in milliseconds """ - normalized_method_name = self._normalize_method_name(method_name) - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): - if self.method_definition_has_integration(method_definition): - method_definition[self._X_APIGW_INTEGRATION]["timeoutInMillis"] = timeout + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): + method_definition[self._X_APIGW_INTEGRATION]["timeoutInMillis"] = timeout def add_path_parameters_to_method(self, api, path, method_name, path_parameters): """ @@ -282,8 +315,7 @@ def add_path_parameters_to_method(self, api, path, method_name, path_parameters) :param string method_name: Method name :param list path_parameters: list of strings of path parameters """ - normalized_method_name = self._normalize_method_name(method_name) - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): # create path parameter list # add it here if it doesn't exist, merge with existing otherwise. method_definition.setdefault("parameters", []) @@ -319,10 +351,8 @@ def add_payload_format_version_to_method(self, api, path, method_name, payload_f :param string method_name: Method name :param string payload_format_version: payload format version sent to the integration """ - normalized_method_name = self._normalize_method_name(method_name) - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): - if self.method_definition_has_integration(method_definition): - method_definition[self._X_APIGW_INTEGRATION]["payloadFormatVersion"] = payload_format_version + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): + method_definition[self._X_APIGW_INTEGRATION]["payloadFormatVersion"] = payload_format_version def add_authorizers_security_definitions(self, authorizers): """ @@ -347,42 +377,45 @@ def set_path_default_authorizer(self, path, default_authorizer, authorizers, api authorizers param. :param list authorizers: List of Authorizer configurations defined on the related Api. """ - for method_name, method in self.get_path(path).items(): - normalized_method_name = self._normalize_method_name(method_name) - # Excluding parameters section - if normalized_method_name == "parameters": - continue - if normalized_method_name != "options": + for path_item in self.get_conditional_contents(self.paths.get(path)): + for method_name, method in path_item.items(): normalized_method_name = self._normalize_method_name(method_name) - # It is possible that the method could have two definitions in a Fn::If block. - if normalized_method_name not in self.get_path(path): - raise InvalidDocumentException( - [ - InvalidTemplateException( - "Could not find {} in {} within DefinitionBody.".format(normalized_method_name, path) - ) - ] - ) - for method_definition in self.get_method_contents(method): - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue - existing_security = method_definition.get("security", []) - if existing_security: - continue - authorizer_list = [] - if authorizers: - authorizer_list.extend(authorizers.keys()) - security_dict = dict() - security_dict[default_authorizer] = self._get_authorization_scopes( - api_authorizers, default_authorizer - ) - authorizer_security = [security_dict] + # Excluding parameters section + if normalized_method_name == "parameters": + continue + if normalized_method_name != "options": + normalized_method_name = self._normalize_method_name(method_name) + # It is possible that the method could have two definitions in a Fn::If block. + if normalized_method_name not in path_item: + raise InvalidDocumentException( + [ + InvalidTemplateException( + "Could not find {} in {} within DefinitionBody.".format( + normalized_method_name, path + ) + ) + ] + ) + for method_definition in self.get_conditional_contents(method): + # If no integration given, then we don't need to process this definition (could be AWS::NoValue) + if not self.method_definition_has_integration(method_definition): + continue + existing_security = method_definition.get("security", []) + if existing_security: + continue + authorizer_list = [] + if authorizers: + authorizer_list.extend(authorizers.keys()) + security_dict = dict() + security_dict[default_authorizer] = self._get_authorization_scopes( + api_authorizers, default_authorizer + ) + authorizer_security = [security_dict] - security = authorizer_security + security = authorizer_security - if security: - method_definition["security"] = security + if security: + method_definition["security"] = security def add_auth_to_method(self, path, method_name, auth, api): """ @@ -415,14 +448,8 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers """ if authorization_scopes is None: authorization_scopes = [] - normalized_method_name = self._normalize_method_name(method_name) - # It is possible that the method could have two definitions in a Fn::If block. - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): - - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): existing_security = method_definition.get("security", []) security_dict = dict() diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 0fce61221..e8f57fc3b 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -2,8 +2,7 @@ import json import re -from samtranslator.model.intrinsics import ref -from samtranslator.model.intrinsics import make_conditional, fnSub +from samtranslator.model.intrinsics import ref, make_conditional, fnSub, is_intrinsic_no_value from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr @@ -59,44 +58,58 @@ def __init__(self, doc): # We can do an early path validation on path item objects, # so we don't need to validate wherever we use them. for path in self.iter_on_path(): - SwaggerEditor.validate_path_item_is_dict(self.get_path(path), path) + for path_item in self.get_conditional_contents(self.paths.get(path)): + SwaggerEditor.validate_path_item_is_dict(path_item, path) self.security_definitions = self._doc.get("securityDefinitions", Py27Dict()) self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, Py27Dict()) self.resource_policy = self._doc.get(self._X_APIGW_POLICY, Py27Dict()) self.definitions = self._doc.get("definitions", Py27Dict()) - def get_path(self, path): - path_dict = self.paths.get(path) - if isinstance(path_dict, dict) and self._CONDITIONAL_IF in path_dict: - path_dict = path_dict[self._CONDITIONAL_IF][1] - return path_dict + def get_conditional_contents(self, item): + """ + Returns the contents of the given item. + If a conditional block has been used inside the item, returns a list of the content + inside the conditional (both the then and the else cases). Skips {'Ref': 'AWS::NoValue'} content. + If there's no conditional block, then returns an list with the single item in it. + + :param dict item: item from which the contents will be extracted + :return: list of item content + """ + contents = [item] + if isinstance(item, dict) and self._CONDITIONAL_IF in item: + contents = item[self._CONDITIONAL_IF][1:] + contents = [content for content in contents if not is_intrinsic_no_value(content)] + return contents def has_path(self, path, method=None): """ Returns True if this Swagger has the given path and optional method + For paths with conditionals, only returns true if both items (true case, and false case) have the method. :param string path: Path name :param string method: HTTP method :return: True, if this path/method is present in the document """ - method = self._normalize_method_name(method) + if path not in self.paths: + return False - path_dict = self.get_path(path) - path_dict_exists = path_dict is not None + method = self._normalize_method_name(method) if method: - return path_dict_exists and method in path_dict - return path_dict_exists + for path_item in self.get_conditional_contents(self.paths.get(path)): + if method not in path_item: + return False + return True def method_has_integration(self, method): """ Returns true if the given method contains a valid method definition. - This uses the get_method_contents function to handle conditionals. + This uses the get_conditional_contents function to handle conditionals. :param dict method: method dictionary :return: true if method has one or multiple integrations """ - for method_definition in self.get_method_contents(method): + for method_definition in self.get_conditional_contents(method): if self.method_definition_has_integration(method_definition): return True return False @@ -112,19 +125,6 @@ def method_definition_has_integration(self, method_definition): return True return False - def get_method_contents(self, method): - """ - Returns the swagger contents of the given method. This checks to see if a conditional block - has been used inside of the method, and, if so, returns the method contents that are - inside of the conditional. - - :param dict method: method dictionary - :return: list of swagger component dictionaries for the method - """ - if self._CONDITIONAL_IF in method: - return method[self._CONDITIONAL_IF][1:] - return [method] - def add_disable_execute_api_endpoint_extension(self, disable_execute_api_endpoint): """Add endpoint configuration to _X_APIGW_ENDPOINT_CONFIG in open api definition as extension Following this guide: @@ -140,7 +140,8 @@ def add_disable_execute_api_endpoint_extension(self, disable_execute_api_endpoin def has_integration(self, path, method): """ - Checks if an API Gateway integration is already present at the given path/method + Checks if an API Gateway integration is already present at the given path/method. + For paths with conditionals, it only returns True if both items (true case, false case) have the integration :param string path: Path name :param string method: HTTP method @@ -148,12 +149,15 @@ def has_integration(self, path, method): """ method = self._normalize_method_name(method) - path_dict = self.get_path(path) - return ( - self.has_path(path, method) - and isinstance(path_dict[method], dict) - and self.method_has_integration(path_dict[method]) - ) # Integration present and non-empty + if not self.has_path(path, method): + return False + + for path_item in self.get_conditional_contents(self.paths.get(path)): + method_definition = path_item.get(method) + if not (isinstance(method_definition, dict) and self.method_has_integration(method_definition)): + return False + # Integration present and non-empty + return True def add_path(self, path, method=None): """ @@ -164,13 +168,10 @@ def add_path(self, path, method=None): """ method = self._normalize_method_name(method) - path_dict = self.paths.setdefault(path, Py27Dict()) - SwaggerEditor.validate_path_item_is_dict(path_dict, path) - - if self._CONDITIONAL_IF in path_dict: - path_dict = path_dict[self._CONDITIONAL_IF][1] + self.paths.setdefault(path, Py27Dict()) - path_dict.setdefault(method, Py27Dict()) + for path_item in self.get_conditional_contents(self.paths.get(path)): + path_item.setdefault(method, Py27Dict()) def add_lambda_integration( self, path, method, integration_uri, method_auth_config=None, api_auth_config=None, condition=None @@ -194,38 +195,38 @@ def add_lambda_integration( if condition: integration_uri = make_conditional(condition, integration_uri) - path_dict = self.get_path(path) - path_dict[method][self._X_APIGW_INTEGRATION] = Py27Dict() - # insert key one by one to preserce input order - path_dict[method][self._X_APIGW_INTEGRATION]["type"] = "aws_proxy" - path_dict[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" - path_dict[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri - - method_auth_config = method_auth_config or Py27Dict() - api_auth_config = api_auth_config or Py27Dict() - if ( - method_auth_config.get("Authorizer") == "AWS_IAM" - or api_auth_config.get("DefaultAuthorizer") == "AWS_IAM" - and not method_auth_config - ): - method_invoke_role = method_auth_config.get("InvokeRole") - if not method_invoke_role and "InvokeRole" in method_auth_config: - method_invoke_role = "NONE" - api_invoke_role = api_auth_config.get("InvokeRole") - if not api_invoke_role and "InvokeRole" in api_auth_config: - api_invoke_role = "NONE" - credentials = self._generate_integration_credentials( - method_invoke_role=method_invoke_role, api_invoke_role=api_invoke_role - ) - if credentials and credentials != "NONE": - self.paths[path][method][self._X_APIGW_INTEGRATION]["credentials"] = credentials + for path_item in self.get_conditional_contents(self.paths.get(path)): + path_item[method][self._X_APIGW_INTEGRATION] = Py27Dict() + # insert key one by one to preserce input order + path_item[method][self._X_APIGW_INTEGRATION]["type"] = "aws_proxy" + path_item[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" + path_item[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri - # If 'responses' key is *not* present, add it with an empty dict as value - path_dict[method].setdefault("responses", Py27Dict()) + method_auth_config = method_auth_config or Py27Dict() + api_auth_config = api_auth_config or Py27Dict() + if ( + method_auth_config.get("Authorizer") == "AWS_IAM" + or api_auth_config.get("DefaultAuthorizer") == "AWS_IAM" + and not method_auth_config + ): + method_invoke_role = method_auth_config.get("InvokeRole") + if not method_invoke_role and "InvokeRole" in method_auth_config: + method_invoke_role = "NONE" + api_invoke_role = api_auth_config.get("InvokeRole") + if not api_invoke_role and "InvokeRole" in api_auth_config: + api_invoke_role = "NONE" + credentials = self._generate_integration_credentials( + method_invoke_role=method_invoke_role, api_invoke_role=api_invoke_role + ) + if credentials and credentials != "NONE": + path_item[method][self._X_APIGW_INTEGRATION]["credentials"] = credentials - # If a condition is present, wrap all method contents up into the condition - if condition: - path_dict[method] = make_conditional(condition, path_dict[method]) + # If 'responses' key is *not* present, add it with an empty dict as value + path_item[method].setdefault("responses", Py27Dict()) + + # If a condition is present, wrap all method contents up into the condition + if condition: + path_item[method] = make_conditional(condition, path_item[method]) def add_state_machine_integration( self, @@ -258,36 +259,35 @@ def add_state_machine_integration( if condition: integration_uri = make_conditional(condition, integration_uri) - path_dict = self.get_path(path) - - # Responses - integration_responses = Py27Dict() - # insert key one by one to preserce input order - integration_responses["200"] = Py27Dict({"statusCode": "200"}) - integration_responses["400"] = Py27Dict({"statusCode": "400"}) + for path_item in self.get_conditional_contents(self.paths.get(path)): + # Responses + integration_responses = Py27Dict() + # insert key one by one to preserce input order + integration_responses["200"] = Py27Dict({"statusCode": "200"}) + integration_responses["400"] = Py27Dict({"statusCode": "400"}) - default_method_responses = Py27Dict() - # insert key one by one to preserce input order - default_method_responses["200"] = Py27Dict({"description": "OK"}) - default_method_responses["400"] = Py27Dict({"description": "Bad Request"}) + default_method_responses = Py27Dict() + # insert key one by one to preserce input order + default_method_responses["200"] = Py27Dict({"description": "OK"}) + default_method_responses["400"] = Py27Dict({"description": "Bad Request"}) - path_dict[method][self._X_APIGW_INTEGRATION] = Py27Dict() - # insert key one by one to preserce input order - path_dict[method][self._X_APIGW_INTEGRATION]["type"] = "aws" - path_dict[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" - path_dict[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri - path_dict[method][self._X_APIGW_INTEGRATION]["responses"] = integration_responses - path_dict[method][self._X_APIGW_INTEGRATION]["credentials"] = credentials + path_item[method][self._X_APIGW_INTEGRATION] = Py27Dict() + # insert key one by one to preserce input order + path_item[method][self._X_APIGW_INTEGRATION]["type"] = "aws" + path_item[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" + path_item[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri + path_item[method][self._X_APIGW_INTEGRATION]["responses"] = integration_responses + path_item[method][self._X_APIGW_INTEGRATION]["credentials"] = credentials - # If 'responses' key is *not* present, add it with an empty dict as value - path_dict[method].setdefault("responses", default_method_responses) + # If 'responses' key is *not* present, add it with an empty dict as value + path_item[method].setdefault("responses", default_method_responses) - if request_templates: - path_dict[method][self._X_APIGW_INTEGRATION].update({"requestTemplates": request_templates}) + if request_templates: + path_item[method][self._X_APIGW_INTEGRATION].update({"requestTemplates": request_templates}) - # If a condition is present, wrap all method contents up into the condition - if condition: - path_dict[method] = make_conditional(condition, path_dict[method]) + # If a condition is present, wrap all method contents up into the condition + if condition: + path_item[method] = make_conditional(condition, path_item[method]) def make_path_conditional(self, path, condition): """ @@ -313,6 +313,56 @@ def iter_on_path(self): for path, value in self.paths.items(): yield path + def iter_on_method_definitions_for_path_at_method( + self, path_name, method_name, skip_methods_without_apigw_integration=True + ): + """ + Yields all the method definitions for the path+method combinations if path and/or method have IF conditionals. + If there are no conditionals, will just yield the single method definition at the given path and method name. + + :param path_name: path name + :param method_name: method name + :param skip_methods_without_apigw_integration: if True, skips method definitions without apigw integration + :yields dict: method definition + """ + normalized_method_name = self._normalize_method_name(method_name) + + for path_item in self.get_conditional_contents(self.paths.get(path_name)): + for method_definition in self.get_conditional_contents(path_item.get(normalized_method_name)): + if skip_methods_without_apigw_integration and not self.method_definition_has_integration( + method_definition + ): + continue + yield method_definition + + def iter_on_all_methods_for_path(self, path_name, skip_methods_without_apigw_integration=True): + """ + Yields all the (method name, method definition) tuples for the path, including those inside conditionals. + + :param path_name: path name + :param skip_methods_without_apigw_integration: if True, skips method definitions without apigw integration + :yields list of (method name, method definition) tuples + """ + for path_item in self.get_conditional_contents(self.paths.get(path_name)): + for method_name, method in path_item.items(): + # Excluding non-method sections + if method_name in SwaggerEditor._EXCLUDED_PATHS_FIELDS: + continue + + for method_definition in self.get_conditional_contents(method): + SwaggerEditor.validate_is_dict( + method_definition, + 'Value of "{}" ({}) for path {} is not a valid dictionary.'.format( + method_name, method_definition, path_name + ), + ) + if skip_methods_without_apigw_integration and not self.method_definition_has_integration( + method_definition + ): + continue + normalized_method_name = self._normalize_method_name(method_name) + yield normalized_method_name, method_definition + def add_cors( self, path, allowed_origins, allowed_headers=None, allowed_methods=None, max_age=None, allow_credentials=None ): @@ -340,28 +390,29 @@ def add_cors( :raises ValueError: When values for one of the allowed_* variables is empty """ - # Skip if Options is already present - if self.has_path(path, self._OPTIONS_METHOD): - return + for path_item in self.get_conditional_contents(self.paths.get(path)): + # Skip if Options is already present + method = self._normalize_method_name(self._OPTIONS_METHOD) + if method in path_item: + continue - if not allowed_origins: - raise InvalidTemplateException("Invalid input. Value for AllowedOrigins is required") + if not allowed_origins: + raise InvalidTemplateException("Invalid input. Value for AllowedOrigins is required") - if not allowed_methods: - # AllowMethods is not given. Let's try to generate the list from the given Swagger. - allowed_methods = self._make_cors_allowed_methods_for_path(path) + if not allowed_methods: + # AllowMethods is not given. Let's try to generate the list from the given Swagger. + allowed_methods = self._make_cors_allowed_methods_for_path_item(path_item) - # APIGW expects the value to be a "string expression". Hence wrap in another quote. Ex: "'GET,POST,DELETE'" - allowed_methods = "'{}'".format(allowed_methods) + # APIGW expects the value to be a "string expression". Hence wrap in another quote. Ex: "'GET,POST,DELETE'" + allowed_methods = "'{}'".format(allowed_methods) - if allow_credentials is not True: - allow_credentials = False + if allow_credentials is not True: + allow_credentials = False - # Add the Options method and the CORS response - self.add_path(path, self._OPTIONS_METHOD) - self.get_path(path)[self._OPTIONS_METHOD] = self._options_method_response_for_cors( - allowed_origins, allowed_headers, allowed_methods, max_age, allow_credentials - ) + # Add the Options method and the CORS response + path_item[self._OPTIONS_METHOD] = self._options_method_response_for_cors( + allowed_origins, allowed_headers, allowed_methods, max_age, allow_credentials + ) def add_binary_media_types(self, binary_media_types): """ @@ -469,22 +520,17 @@ def _options_method_response_for_cors( to_return["responses"]["200"]["headers"] = response_headers return to_return - def _make_cors_allowed_methods_for_path(self, path): + def _make_cors_allowed_methods_for_path_item(self, path_item): """ - Creates the value for Access-Control-Allow-Methods header for given path. All HTTP methods defined for this - path will be included in the result. If the path contains "ANY" method, then *all available* HTTP methods will + Creates the value for Access-Control-Allow-Methods header for given path item. All HTTP methods defined for this + path item will be included in the result. If the path item contains "ANY" method, then *all available* HTTP methods will be returned as result. - :param string path: Path to generate AllowMethods value for - :return string: String containing the value of AllowMethods, if the path contains any methods. - Empty string, otherwise + :param dict path_item: Path item to generate AllowMethods value for + :return string: String containing the value of AllowMethods, if the path item contains any methods. + "OPTIONS", otherwise """ - - if not self.has_path(path): - return "" - - # At this point, value of Swagger path should be a dictionary with method names being the keys - methods = list(self.get_path(path).keys()) + methods = list(path_item.keys()) if self._X_ANY_METHOD in methods: # API Gateway's ANY method is not a real HTTP method but a wildcard representing all HTTP methods @@ -577,89 +623,67 @@ def set_path_default_authorizer( authorizer to OPTIONS preflight requests. """ - for method_name, method in self.get_path(path).items(): - normalized_method_name = self._normalize_method_name(method_name) - - # Excluding non-method sections - if normalized_method_name in SwaggerEditor._EXCLUDED_PATHS_FIELDS: + for method_name, method_definition in self.iter_on_all_methods_for_path(path): + if not (add_default_auth_to_preflight or method_name != "options"): continue - if add_default_auth_to_preflight or normalized_method_name != "options": + existing_security = method_definition.get("security", []) + authorizer_list = ["AWS_IAM"] + if authorizers: + authorizer_list.extend(authorizers.keys()) + authorizer_names = set(authorizer_list) + existing_non_authorizer_security = [] + existing_authorizer_security = [] + + # Split existing security into Authorizers and everything else + # (e.g. sigv4 (AWS_IAM), api_key (API Key/Usage Plans), NONE (marker for ignoring default)) + # We want to ensure only a single Authorizer security entry exists while keeping everything else + for security in existing_security: SwaggerEditor.validate_is_dict( - method, - 'Value of "{}" ({}) for path {} is not a valid dictionary.'.format(method_name, method, path), + security, "{} in Security for path {} is not a valid dictionary.".format(security, path) ) - # It is possible that the method could have two definitions in a Fn::If block. - for method_definition in self.get_method_contents(method): + if authorizer_names.isdisjoint(security.keys()): + existing_non_authorizer_security.append(security) + else: + existing_authorizer_security.append(security) - SwaggerEditor.validate_is_dict( - method_definition, - 'Value of "{}" ({}) for path {} is not a valid dictionary.'.format( - method_name, method_definition, path - ), - ) - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue - existing_security = method_definition.get("security", []) - authorizer_list = ["AWS_IAM"] - if authorizers: - authorizer_list.extend(authorizers.keys()) - authorizer_names = set(authorizer_list) - existing_non_authorizer_security = [] - existing_authorizer_security = [] - - # Split existing security into Authorizers and everything else - # (e.g. sigv4 (AWS_IAM), api_key (API Key/Usage Plans), NONE (marker for ignoring default)) - # We want to ensure only a single Authorizer security entry exists while keeping everything else - for security in existing_security: - SwaggerEditor.validate_is_dict( - security, "{} in Security for path {} is not a valid dictionary.".format(security, path) - ) - if authorizer_names.isdisjoint(security.keys()): - existing_non_authorizer_security.append(security) - else: - existing_authorizer_security.append(security) - - none_idx = -1 - authorizer_security = [] - - # Check for an existing Authorizer before applying the default. It would be simpler - # if instead we applied the DefaultAuthorizer first and then simply - # overwrote it if necessary, however, the order in which things get - # applied (Function Api Events first; then Api Resource) complicates it. - # Check if Function/Path/Method specified 'NONE' for Authorizer - for idx, security in enumerate(existing_non_authorizer_security): - is_none = any(key == "NONE" for key in security.keys()) - - if is_none: - none_idx = idx - break - - # NONE was found; remove it and don't add the DefaultAuthorizer - if none_idx > -1: - del existing_non_authorizer_security[none_idx] - - # Existing Authorizer found (defined at Function/Path/Method); use that instead of default - elif existing_authorizer_security: - authorizer_security = existing_authorizer_security - - # No existing Authorizer found; use default - else: - security_dict = Py27Dict() - security_dict[default_authorizer] = self._get_authorization_scopes( - api_authorizers, default_authorizer - ) - authorizer_security = [security_dict] - - security = existing_non_authorizer_security + authorizer_security - - if security: - method_definition["security"] = security - - # The first element of the method_definition['security'] should be AWS_IAM - # because authorizer_list = ['AWS_IAM'] is hardcoded above - if "AWS_IAM" in method_definition["security"][0]: - self.add_awsiam_security_definition() + none_idx = -1 + authorizer_security = [] + + # Check for an existing Authorizer before applying the default. It would be simpler + # if instead we applied the DefaultAuthorizer first and then simply + # overwrote it if necessary, however, the order in which things get + # applied (Function Api Events first; then Api Resource) complicates it. + # Check if Function/Path/Method specified 'NONE' for Authorizer + for idx, security in enumerate(existing_non_authorizer_security): + is_none = any(key == "NONE" for key in security.keys()) + + if is_none: + none_idx = idx + break + + # NONE was found; remove it and don't add the DefaultAuthorizer + if none_idx > -1: + del existing_non_authorizer_security[none_idx] + + # Existing Authorizer found (defined at Function/Path/Method); use that instead of default + elif existing_authorizer_security: + authorizer_security = existing_authorizer_security + + # No existing Authorizer found; use default + else: + security_dict = Py27Dict() + security_dict[default_authorizer] = self._get_authorization_scopes(api_authorizers, default_authorizer) + authorizer_security = [security_dict] + + security = existing_non_authorizer_security + authorizer_security + + if security: + method_definition["security"] = security + + # The first element of the method_definition['security'] should be AWS_IAM + # because authorizer_list = ['AWS_IAM'] is hardcoded above + if "AWS_IAM" in method_definition["security"][0]: + self.add_awsiam_security_definition() def set_path_default_apikey_required(self, path): """ @@ -671,60 +695,49 @@ def set_path_default_apikey_required(self, path): :param string path: Path name """ - for method_name, method in self.get_path(path).items(): - # Excluding non-method sections - if method_name in SwaggerEditor._EXCLUDED_PATHS_FIELDS: - continue - - # It is possible that the method could have two definitions in a Fn::If block. - for method_definition in self.get_method_contents(method): - - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue - - existing_security = method_definition.get("security", []) - apikey_security_names = set(["api_key", "api_key_false"]) - existing_non_apikey_security = [] - existing_apikey_security = [] - apikey_security = [] - - # Split existing security into ApiKey and everything else - # (e.g. sigv4 (AWS_IAM), authorizers, NONE (marker for ignoring default authorizer)) - # We want to ensure only a single ApiKey security entry exists while keeping everything else - for security in existing_security: - if apikey_security_names.isdisjoint(security.keys()): - existing_non_apikey_security.append(security) - else: - existing_apikey_security.append(security) - - # Check for an existing method level ApiKey setting before applying the default. It would be simpler - # if instead we applied the default first and then simply - # overwrote it if necessary, however, the order in which things get - # applied (Function Api Events first; then Api Resource) complicates it. - # Check if Function/Path/Method specified 'False' for ApiKeyRequired - apikeyfalse_idx = -1 - for idx, security in enumerate(existing_apikey_security): - is_none = any(key == "api_key_false" for key in security.keys()) - - if is_none: - apikeyfalse_idx = idx - break - - # api_key_false was found; remove it and don't add default api_key security setting - if apikeyfalse_idx > -1: - del existing_apikey_security[apikeyfalse_idx] - - # No existing ApiKey setting found or it's already set to the default + for _, method_definition in self.iter_on_all_methods_for_path(path): + existing_security = method_definition.get("security", []) + apikey_security_names = set(["api_key", "api_key_false"]) + existing_non_apikey_security = [] + existing_apikey_security = [] + apikey_security = [] + + # Split existing security into ApiKey and everything else + # (e.g. sigv4 (AWS_IAM), authorizers, NONE (marker for ignoring default authorizer)) + # We want to ensure only a single ApiKey security entry exists while keeping everything else + for security in existing_security: + if apikey_security_names.isdisjoint(security.keys()): + existing_non_apikey_security.append(security) else: - security_dict = Py27Dict() - security_dict["api_key"] = [] - apikey_security = [security_dict] + existing_apikey_security.append(security) + + # Check for an existing method level ApiKey setting before applying the default. It would be simpler + # if instead we applied the default first and then simply + # overwrote it if necessary, however, the order in which things get + # applied (Function Api Events first; then Api Resource) complicates it. + # Check if Function/Path/Method specified 'False' for ApiKeyRequired + apikeyfalse_idx = -1 + for idx, security in enumerate(existing_apikey_security): + is_none = any(key == "api_key_false" for key in security.keys()) + + if is_none: + apikeyfalse_idx = idx + break + + # api_key_false was found; remove it and don't add default api_key security setting + if apikeyfalse_idx > -1: + del existing_apikey_security[apikeyfalse_idx] + + # No existing ApiKey setting found or it's already set to the default + else: + security_dict = Py27Dict() + security_dict["api_key"] = [] + apikey_security = [security_dict] - security = existing_non_apikey_security + apikey_security + security = existing_non_apikey_security + apikey_security - if security != existing_security: - method_definition["security"] = security + if security != existing_security: + method_definition["security"] = security def add_auth_to_method(self, path, method_name, auth, api): """ @@ -760,14 +773,8 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers """ if authorizers is None: authorizers = Py27Dict() - normalized_method_name = self._normalize_method_name(method_name) - # It is possible that the method could have two definitions in a Fn::If block. - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): - - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): existing_security = method_definition.get("security", []) security_dict = Py27Dict() @@ -801,14 +808,7 @@ def _set_method_apikey_handling(self, path, method_name, apikey_required): :param string method_name: Method name :param bool apikey_required: Whether the apikey security is required """ - normalized_method_name = self._normalize_method_name(method_name) - # It is possible that the method could have two definitions in a Fn::If block. - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): - - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue - + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): existing_security = method_definition.get("security", []) if apikey_required: @@ -841,7 +841,6 @@ def add_request_validator_to_method(self, path, method_name, validate_body=False :param bool validate_parameters: Validate request """ - normalized_method_name = self._normalize_method_name(method_name) validator_name = SwaggerEditor.get_validator_name(validate_body, validate_parameters) # Creating validator as py27 dict @@ -858,21 +857,10 @@ def add_request_validator_to_method(self, path, method_name, validate_body=False # Adding only if the validator hasn't been defined already self._doc[self._X_APIGW_REQUEST_VALIDATORS].update(request_validator_definition) - # It is possible that the method could have two definitions in a Fn::If block. - for path_method_name, method in self.get_path(path).items(): - normalized_path_method_name = self._normalize_method_name(path_method_name) - - # Adding it to only given method to the path - if normalized_path_method_name == normalized_method_name: - for method_definition in self.get_method_contents(method): - - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue - - set_validator_to_method = Py27Dict({self._X_APIGW_REQUEST_VALIDATOR: validator_name}) - # Setting validator to the given method - method_definition.update(set_validator_to_method) + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): + set_validator_to_method = Py27Dict({self._X_APIGW_REQUEST_VALIDATOR: validator_name}) + # Setting validator to the given method + method_definition.update(set_validator_to_method) def add_request_model_to_method(self, path, method_name, request_model): """ @@ -885,14 +873,7 @@ def add_request_model_to_method(self, path, method_name, request_model): model_name = request_model and request_model.get("Model").lower() model_required = request_model and request_model.get("Required") - normalized_method_name = self._normalize_method_name(method_name) - # It is possible that the method could have two definitions in a Fn::If block. - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): - - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue - + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): if self._doc.get("swagger") is not None: existing_parameters = method_definition.get("parameters", []) @@ -1078,7 +1059,9 @@ def _get_method_path_uri_list(self, path, stage): and removes as a part of their behavior, but this isn't documented. The regex removes the trailing slash to ensure the permission works as intended """ - methods = list(self.get_path(path).keys()) + methods = [] + for path_item in self.get_conditional_contents(self.paths.get(path)): + methods += list(path_item.keys()) uri_list = [] path = SwaggerEditor.get_path_without_trailing_slash(path) @@ -1228,14 +1211,7 @@ def add_request_parameters_to_method(self, path, method_name, request_parameters :return: """ - normalized_method_name = self._normalize_method_name(method_name) - # It is possible that the method could have two definitions in a Fn::If block. - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): - - # If no integration given, then we don't need to process this definition (could be AWS::NoValue) - if not self.method_definition_has_integration(method_definition): - continue - + for method_definition in self.iter_on_method_definitions_for_path_at_method(path, method_name): existing_parameters = method_definition.get("parameters", []) for request_parameter in request_parameters: diff --git a/tests/openapi/test_openapi.py b/tests/openapi/test_openapi.py index 99197de9c..2c89afc06 100644 --- a/tests/openapi/test_openapi.py +++ b/tests/openapi/test_openapi.py @@ -367,7 +367,7 @@ def setUp(self): self.editor = OpenApiEditor(self.original_openapi) -class TestOpenApiEditor_get_integration_function(TestCase): +class TestOpenApiEditor_is_integration_function_logical_id_match(TestCase): def setUp(self): self.original_openapi = { @@ -407,13 +407,14 @@ def setUp(self): self.editor = OpenApiEditor(self.original_openapi) - def test_must_get_integration_function_if_exists(self): + def test_must_match_integration_function_if_exists(self): - self.assertEqual( - self.editor.get_integration_function_logical_id(OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD), - "HttpApiFunction", + self.assertTrue( + self.editor.is_integration_function_logical_id_match( + OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD, "HttpApiFunction" + ), ) - self.assertFalse(self.editor.get_integration_function_logical_id("/bar", "get")) + self.assertFalse(self.editor.is_integration_function_logical_id_match("/bar", "get", "HttpApiFunction")) class TestOpenApiEdit_add_description(TestCase): @@ -439,7 +440,7 @@ def test_must_not_add_description_if_already_defined(self): self.assertEqual(editor.openapi["info"]["description"], "Existing Description") -class TestOpenApiEditor_get_integration_function_of_alias(TestCase): +class TestOpenApiEditor_is_integration_function_logical_id_match_with_alias(TestCase): def setUp(self): self.original_openapi = { @@ -479,8 +480,10 @@ def setUp(self): self.editor = OpenApiEditor(self.original_openapi) - def test_no_logical_id_if_alias(self): + def test_no_match_if_alias(self): self.assertFalse( - self.editor.get_integration_function_logical_id(OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD), + self.editor.is_integration_function_logical_id_match( + OpenApiEditor._DEFAULT_PATH, OpenApiEditor._X_ANY_METHOD, "HttpApiFunctionAlias" + ), ) diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index d5a6e1ada..3214b5c2d 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -385,8 +385,8 @@ def test_must_make_default_value_with_optional_allowed_methods(self): expected = {"some cors": "return value"} path = "/foo" - self.editor._make_cors_allowed_methods_for_path = Mock() - self.editor._make_cors_allowed_methods_for_path.return_value = default_allow_methods_value + self.editor._make_cors_allowed_methods_for_path_item = Mock() + self.editor._make_cors_allowed_methods_for_path_item.return_value = default_allow_methods_value self.editor._options_method_response_for_cors = Mock() self.editor._options_method_response_for_cors.return_value = expected @@ -578,45 +578,39 @@ def test_allow_credentials_is_skipped_with_false_value(self): self.assertEqual(expected, actual) -class TestSwaggerEditor_make_cors_allowed_methods_for_path(TestCase): +class TestSwaggerEditor_make_cors_allowed_methods_for_path_item(TestCase): def setUp(self): + self.foo_path_item = {"get": {}, "POST": {}, "DeLeTe": {}} + self.withany_path_item = {"head": {}, _X_ANY_METHOD: {}} + self.nothing_path_item = {} + self.editor = SwaggerEditor( { "swagger": "2.0", "paths": { - "/foo": {"get": {}, "POST": {}, "DeLeTe": {}}, - "/withany": {"head": {}, _X_ANY_METHOD: {}}, - "/nothing": {}, + "/foo": self.foo_path_item, + "/withany": self.withany_path_item, + "/nothing": self.nothing_path_item, }, } ) def test_must_return_all_defined_methods(self): - path = "/foo" expected = "DELETE,GET,OPTIONS,POST" # Result should be sorted alphabetically - actual = self.editor._make_cors_allowed_methods_for_path(path) + actual = self.editor._make_cors_allowed_methods_for_path_item(self.foo_path_item) self.assertEqual(expected, actual) def test_must_work_for_any_method(self): - path = "/withany" expected = "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT" # Result should be sorted alphabetically - actual = self.editor._make_cors_allowed_methods_for_path(path) + actual = self.editor._make_cors_allowed_methods_for_path_item(self.withany_path_item) self.assertEqual(expected, actual) def test_must_work_with_no_methods(self): - path = "/nothing" expected = "OPTIONS" - actual = self.editor._make_cors_allowed_methods_for_path(path) - self.assertEqual(expected, actual) - - def test_must_skip_non_existent_path(self): - path = "/no-path" - expected = "" - - actual = self.editor._make_cors_allowed_methods_for_path(path) + actual = self.editor._make_cors_allowed_methods_for_path_item(self.nothing_path_item) self.assertEqual(expected, actual) diff --git a/tests/translator/input/api_http_paths_with_if_condition.yaml b/tests/translator/input/api_http_paths_with_if_condition.yaml new file mode 100644 index 000000000..79fed2bba --- /dev/null +++ b/tests/translator/input/api_http_paths_with_if_condition.yaml @@ -0,0 +1,68 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.otherURI.co/ + payloadFormatVersion: '1.0' + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' \ No newline at end of file diff --git a/tests/translator/input/api_http_paths_with_if_condition_no_value_else_case.yaml b/tests/translator/input/api_http_paths_with_if_condition_no_value_else_case.yaml new file mode 100644 index 000000000..822672b9d --- /dev/null +++ b/tests/translator/input/api_http_paths_with_if_condition_no_value_else_case.yaml @@ -0,0 +1,62 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' + - Ref: "AWS::NoValue" \ No newline at end of file diff --git a/tests/translator/input/api_http_paths_with_if_condition_no_value_then_case.yaml b/tests/translator/input/api_http_paths_with_if_condition_no_value_then_case.yaml new file mode 100644 index 000000000..6bb4cf145 --- /dev/null +++ b/tests/translator/input/api_http_paths_with_if_condition_no_value_then_case.yaml @@ -0,0 +1,62 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - Ref: "AWS::NoValue" + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' \ No newline at end of file diff --git a/tests/translator/input/api_rest_paths_with_if_condition_openapi.yaml b/tests/translator/input/api_rest_paths_with_if_condition_openapi.yaml new file mode 100644 index 000000000..facba2739 --- /dev/null +++ b/tests/translator/input/api_rest_paths_with_if_condition_openapi.yaml @@ -0,0 +1,76 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: dev + Auth: + ApiKeyRequired: true + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.otherURI.co/ + payloadFormatVersion: '1.0' + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' \ No newline at end of file diff --git a/tests/translator/input/api_rest_paths_with_if_condition_openapi_no_value_else_case.yaml b/tests/translator/input/api_rest_paths_with_if_condition_openapi_no_value_else_case.yaml new file mode 100644 index 000000000..cef683a76 --- /dev/null +++ b/tests/translator/input/api_rest_paths_with_if_condition_openapi_no_value_else_case.yaml @@ -0,0 +1,70 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: dev + Auth: + ApiKeyRequired: true + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.otherURI.co/ + payloadFormatVersion: '1.0' + - Ref: "AWS::NoValue" \ No newline at end of file diff --git a/tests/translator/input/api_rest_paths_with_if_condition_openapi_no_value_then_case.yaml b/tests/translator/input/api_rest_paths_with_if_condition_openapi_no_value_then_case.yaml new file mode 100644 index 000000000..9dc4719a8 --- /dev/null +++ b/tests/translator/input/api_rest_paths_with_if_condition_openapi_no_value_then_case.yaml @@ -0,0 +1,70 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: dev + Auth: + ApiKeyRequired: true + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - Ref: "AWS::NoValue" + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' \ No newline at end of file diff --git a/tests/translator/input/api_rest_paths_with_if_condition_swagger.yaml b/tests/translator/input/api_rest_paths_with_if_condition_swagger.yaml new file mode 100644 index 000000000..8732c9ba1 --- /dev/null +++ b/tests/translator/input/api_rest_paths_with_if_condition_swagger.yaml @@ -0,0 +1,76 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: dev + Auth: + ApiKeyRequired: true + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + swagger: '2.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.otherURI.co/ + payloadFormatVersion: '1.0' + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' \ No newline at end of file diff --git a/tests/translator/input/api_rest_paths_with_if_condition_swagger_no_value_else_case.yaml b/tests/translator/input/api_rest_paths_with_if_condition_swagger_no_value_else_case.yaml new file mode 100644 index 000000000..32cf0c03c --- /dev/null +++ b/tests/translator/input/api_rest_paths_with_if_condition_swagger_no_value_else_case.yaml @@ -0,0 +1,70 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: dev + Auth: + ApiKeyRequired: true + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + swagger: '2.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.otherURI.co/ + payloadFormatVersion: '1.0' + - Ref: "AWS::NoValue" \ No newline at end of file diff --git a/tests/translator/input/api_rest_paths_with_if_condition_swagger_no_value_then_case.yaml b/tests/translator/input/api_rest_paths_with_if_condition_swagger_no_value_then_case.yaml new file mode 100644 index 000000000..7f94cf1e5 --- /dev/null +++ b/tests/translator/input/api_rest_paths_with_if_condition_swagger_no_value_then_case.yaml @@ -0,0 +1,70 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-app + + Sample SAM Template for sam-app + +Globals: + Api: + Cors: { + "Fn::Join": [",", ["www.amazon.com", "www.google.com"]] + } + +Conditions: + TrueCondition: + Fn::Equals: + - true + - true + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::Api + Properties: + StageName: dev + Auth: + ApiKeyRequired: true + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Authorization + ReauthorizeEvery: 37 + EnableSimpleResponses: false + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + DefinitionBody: + swagger: '2.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + Fn::If: + - FalseCondition + - Ref: "AWS::NoValue" + - + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' \ No newline at end of file diff --git a/tests/translator/output/api_http_paths_with_if_condition.json b/tests/translator/output/api_http_paths_with_if_condition.json new file mode 100644 index 000000000..80957af7a --- /dev/null +++ b/tests/translator/output/api_http_paths_with_if_condition.json @@ -0,0 +1,178 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/api_http_paths_with_if_condition_no_value_else_case.json b/tests/translator/output/api_http_paths_with_if_condition_no_value_else_case.json new file mode 100644 index 000000000..8a8f28e18 --- /dev/null +++ b/tests/translator/output/api_http_paths_with_if_condition_no_value_else_case.json @@ -0,0 +1,166 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/api_http_paths_with_if_condition_no_value_then_case.json b/tests/translator/output/api_http_paths_with_if_condition_no_value_then_case.json new file mode 100644 index 000000000..efaa805a3 --- /dev/null +++ b/tests/translator/output/api_http_paths_with_if_condition_no_value_then_case.json @@ -0,0 +1,166 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/api_rest_paths_with_if_condition_openapi.json b/tests/translator/output/api_rest_paths_with_if_condition_openapi.json new file mode 100644 index 000000000..75b9521f7 --- /dev/null +++ b/tests/translator/output/api_rest_paths_with_if_condition_openapi.json @@ -0,0 +1,324 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymentdd0af0635c" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiDeploymentdd0af0635c": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: dd0af0635cf0797adb5e55505bff8349ddcfe008", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/api_rest_paths_with_if_condition_openapi_no_value_else_case.json b/tests/translator/output/api_rest_paths_with_if_condition_openapi_no_value_else_case.json new file mode 100644 index 000000000..7128d9806 --- /dev/null +++ b/tests/translator/output/api_rest_paths_with_if_condition_openapi_no_value_else_case.json @@ -0,0 +1,253 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiDeployment0f6a89da8b": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 0f6a89da8bf24151eeb6b4041d5561bcc29537e4", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment0f6a89da8b" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/api_rest_paths_with_if_condition_openapi_no_value_then_case.json b/tests/translator/output/api_rest_paths_with_if_condition_openapi_no_value_then_case.json new file mode 100644 index 000000000..e7d4d9513 --- /dev/null +++ b/tests/translator/output/api_rest_paths_with_if_condition_openapi_no_value_then_case.json @@ -0,0 +1,253 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment517aff8ea7" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiDeployment517aff8ea7": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 517aff8ea7a3e6442006b450cc4856b4afe7fe47", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/api_rest_paths_with_if_condition_swagger.json b/tests/translator/output/api_rest_paths_with_if_condition_swagger.json new file mode 100644 index 000000000..017f379dd --- /dev/null +++ b/tests/translator/output/api_rest_paths_with_if_condition_swagger.json @@ -0,0 +1,322 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymenta2f95cf6c6" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyApiDeploymenta2f95cf6c6": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: a2f95cf6c6cded2d08c802cc1c3ecf5e8c93c81e", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/api_rest_paths_with_if_condition_swagger_no_value_else_case.json b/tests/translator/output/api_rest_paths_with_if_condition_swagger_no_value_else_case.json new file mode 100644 index 000000000..c7fd856cd --- /dev/null +++ b/tests/translator/output/api_rest_paths_with_if_condition_swagger_no_value_else_case.json @@ -0,0 +1,251 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiDeployment4cb7772053": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 4cb77720534f1e01dd19505971d13dae3adeab53", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment4cb7772053" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/api_rest_paths_with_if_condition_swagger_no_value_then_case.json b/tests/translator/output/api_rest_paths_with_if_condition_swagger_no_value_then_case.json new file mode 100644 index 000000000..592601fb8 --- /dev/null +++ b/tests/translator/output/api_rest_paths_with_if_condition_swagger_no_value_then_case.json @@ -0,0 +1,251 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment111c760fa1" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiDeployment111c760fa1": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 111c760fa12d6d0063462b3cae3cdd6efb33fbe9", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_http_paths_with_if_condition.json b/tests/translator/output/aws-cn/api_http_paths_with_if_condition.json new file mode 100644 index 000000000..4a37bfeba --- /dev/null +++ b/tests/translator/output/aws-cn/api_http_paths_with_if_condition.json @@ -0,0 +1,178 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_http_paths_with_if_condition_no_value_else_case.json b/tests/translator/output/aws-cn/api_http_paths_with_if_condition_no_value_else_case.json new file mode 100644 index 000000000..625b1d6b3 --- /dev/null +++ b/tests/translator/output/aws-cn/api_http_paths_with_if_condition_no_value_else_case.json @@ -0,0 +1,166 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_http_paths_with_if_condition_no_value_then_case.json b/tests/translator/output/aws-cn/api_http_paths_with_if_condition_no_value_then_case.json new file mode 100644 index 000000000..9348929ad --- /dev/null +++ b/tests/translator/output/aws-cn/api_http_paths_with_if_condition_no_value_then_case.json @@ -0,0 +1,166 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi.json b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi.json new file mode 100644 index 000000000..1c4c0ffa1 --- /dev/null +++ b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi.json @@ -0,0 +1,332 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment7a5169a458" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiDeployment7a5169a458": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 7a5169a458f926127d35761e818069f54fb11c7e", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi_no_value_else_case.json b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi_no_value_else_case.json new file mode 100644 index 000000000..a2e3906c3 --- /dev/null +++ b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi_no_value_else_case.json @@ -0,0 +1,261 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiDeployment295ec3a9fa": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 295ec3a9fa9a5e053050d7b4dbac15a1817604ea", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment295ec3a9fa" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi_no_value_then_case.json b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi_no_value_then_case.json new file mode 100644 index 000000000..1906f8f8e --- /dev/null +++ b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_openapi_no_value_then_case.json @@ -0,0 +1,261 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApiDeploymentca11cc1e55": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: ca11cc1e55c3bb45d10cfaa66b1487834342d5f3", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymentca11cc1e55" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger.json b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger.json new file mode 100644 index 000000000..1eb31d516 --- /dev/null +++ b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger.json @@ -0,0 +1,330 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApiDeployment026bbf40e9": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 026bbf40e99f01b30b76b4864c11b99a6c1c84e1", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment026bbf40e9" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger_no_value_else_case.json b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger_no_value_else_case.json new file mode 100644 index 000000000..888a40345 --- /dev/null +++ b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger_no_value_else_case.json @@ -0,0 +1,259 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymentab66684380" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiDeploymentab66684380": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: ab666843809d95c79e0c51e838e6f480276429c4", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger_no_value_then_case.json b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger_no_value_then_case.json new file mode 100644 index 000000000..28f7fdcab --- /dev/null +++ b/tests/translator/output/aws-cn/api_rest_paths_with_if_condition_swagger_no_value_then_case.json @@ -0,0 +1,259 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApiDeploymente86593dd8d": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: e86593dd8dc88a67504512de32e36fccc9ffa271", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymente86593dd8d" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition.json b/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition.json new file mode 100644 index 000000000..ae99acb98 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition.json @@ -0,0 +1,178 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition_no_value_else_case.json b/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition_no_value_else_case.json new file mode 100644 index 000000000..916ae15b5 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition_no_value_else_case.json @@ -0,0 +1,166 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition_no_value_then_case.json b/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition_no_value_then_case.json new file mode 100644 index 000000000..4ee79d820 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_http_paths_with_if_condition_no_value_then_case.json @@ -0,0 +1,166 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyApi" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + } + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "MyLambdaAuthUpdated": { + "type": "apiKey", + "name": "Unused", + "in": "header", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "authorizerResultTtlInSeconds": 37, + "identitySource": [ + "$request.header.Authorization" + ], + "authorizerPayloadFormatVersion": 1.0 + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi.json b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi.json new file mode 100644 index 000000000..761bd64be --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi.json @@ -0,0 +1,332 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment4a021ff84a" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + }, + "MyApiDeployment4a021ff84a": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 4a021ff84ad1127bd2e89e8288160946fbf1609a", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi_no_value_else_case.json b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi_no_value_else_case.json new file mode 100644 index 000000000..e67420290 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi_no_value_else_case.json @@ -0,0 +1,261 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymentd912e8ed8f" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyApiDeploymentd912e8ed8f": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: d912e8ed8f7e209db2137ff3ce50c6bbe2acc72a", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi_no_value_then_case.json b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi_no_value_then_case.json new file mode 100644 index 000000000..a6fca1b44 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_openapi_no_value_then_case.json @@ -0,0 +1,261 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiDeployment7a57336b93": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 7a57336b93896aa7a8c72e034a952fc1afc9a482", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment7a57336b93" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "openapi": "3.0", + "components": { + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger.json b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger.json new file mode 100644 index 000000000..8c586525c --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger.json @@ -0,0 +1,330 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment12d57c142f" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + }, + "MyApiDeployment12d57c142f": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 12d57c142fc2514cd1e8308cc6067205bd6b1923", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger_no_value_else_case.json b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger_no_value_else_case.json new file mode 100644 index 000000000..3ccc2ccf0 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger_no_value_else_case.json @@ -0,0 +1,259 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymente8d14e9a46" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiDeploymente8d14e9a46": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: e8d14e9a46ec615e88fcee45775372485f8ed243", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otherURI.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + }, + { + "Ref": "AWS::NoValue" + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger_no_value_then_case.json b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger_no_value_then_case.json new file mode 100644 index 000000000..6467820e5 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_rest_paths_with_if_condition_swagger_no_value_then_case.json @@ -0,0 +1,259 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "sam-app\nSample SAM Template for sam-app\n", + "Conditions": { + "TrueCondition": { + "Fn::Equals": [ + true, + true + ] + }, + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "MyApiMyLambdaAuthUpdatedAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApi" + } + } + ] + } + } + }, + "MyApidevStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment9b1182e52b" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + } + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MyApiDeployment9b1182e52b": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 9b1182e52bcd94d4984db4a1844447cbc60968e5", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + } + }, + "MyApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "Fn::If": [ + "FalseCondition", + { + "Ref": "AWS::NoValue" + }, + { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + }, + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ] + }, + "options": { + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ], + "x-amazon-apigateway-integration": { + "type": "mock", + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "statusCode": "200", + "responseTemplates": { + "application/json": "{}\n" + }, + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": { + "Fn::Join": [ + ",", + [ + "www.amazon.com", + "www.google.com" + ] + ] + }, + "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,POST'" + } + } + } + }, + "summary": "CORS support", + "security": [ + { + "MyLambdaAuthUpdated": [] + }, + { + "api_key": [] + } + ], + "consumes": [ + "application/json" + ] + } + } + ] + } + }, + "swagger": "2.0", + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "x-api-key", + "in": "header" + }, + "MyLambdaAuthUpdated": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerResultTtlInSeconds": 37, + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerCredentials": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } + } + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index afb2eb47e..f94534464 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -461,6 +461,15 @@ class TestTranslatorEndToEnd(AbstractTestTranslator): "api_with_security_definition_and_components", "api_with_security_definition_and_none_components", "api_with_security_definition_and_no_components", + "api_http_paths_with_if_condition", + "api_http_paths_with_if_condition_no_value_then_case", + "api_http_paths_with_if_condition_no_value_else_case", + "api_rest_paths_with_if_condition_swagger", + "api_rest_paths_with_if_condition_swagger_no_value_then_case", + "api_rest_paths_with_if_condition_swagger_no_value_else_case", + "api_rest_paths_with_if_condition_openapi", + "api_rest_paths_with_if_condition_openapi_no_value_then_case", + "api_rest_paths_with_if_condition_openapi_no_value_else_case", ], [ ("aws", "ap-southeast-1"), From dfb1307e7e7b9059bb73c9c96f4e4d9c64f0f6af Mon Sep 17 00:00:00 2001 From: marekaiv <85357404+marekaiv@users.noreply.github.com> Date: Thu, 24 Feb 2022 09:28:32 -0800 Subject: [PATCH 49/59] Py27dict deepcopy performance (#2331) * Implement __deepcopy__ to improve performance * Cache py27 hash for Py27UniStr to improve performance --- samtranslator/utils/py27hash_fix.py | 44 +++++++++++++++++++++++++++-- tests/utils/test_py27hash_fix.py | 14 +++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/samtranslator/utils/py27hash_fix.py b/samtranslator/utils/py27hash_fix.py index 0172784ff..70feea6ad 100644 --- a/samtranslator/utils/py27hash_fix.py +++ b/samtranslator/utils/py27hash_fix.py @@ -133,6 +133,15 @@ def replace(self, __old, __new, __count=None): def split(self, sep=None, maxsplit=-1): return [Py27UniStr(s) for s in super(Py27UniStr, self).split(sep, maxsplit)] + def __deepcopy__(self, memo): + return self # strings are immutable + + def _get_py27_hash(self): + h = getattr(self, "_py27_hash", None) + if h is None: + self._py27_hash = h = ctypes.c_size_t(Hash.hash(self)).value + return h + class Py27LongInt(long_int_type): """ @@ -147,6 +156,9 @@ def __repr__(self): return super(Py27LongInt, self).__repr__() + "L" return super(Py27LongInt, self).__repr__() + def __deepcopy__(self, memo): + return self # primitive types (ints) are immutable + class Py27Keys(object): """ @@ -167,17 +179,32 @@ def __init__(self): self.fill = 0 # increment count when a key is added, equivalent to ma_fill in dictobject.c self.mask = MINSIZE - 1 # Python2 default dict size + def __deepcopy__(self, memo): + # add keys in the py2 order -- we can't do a straigh-up deep copy of keyorder because + # in py2 copy.deepcopy of a dict may result in reordering of the keys + ret = Py27Keys() + for k in self.keys(): + if k is self.DUMMY: + continue + ret.add(copy.deepcopy(k, memo)) + return ret + def _get_key_idx(self, k): """Gets insert location for k""" - freeslot = None - # C API uses unsigned values - h = ctypes.c_size_t(Hash.hash(k)).value + + # Py27UniStr caches the hash to improve performance so use its method instead of always computing the hash + if isinstance(k, Py27UniStr): + h = k._get_py27_hash() + else: + h = ctypes.c_size_t(Hash.hash(k)).value + i = h & self.mask if i not in self.keyorder or self.keyorder[i] == k: # empty slot or keys match return i + freeslot = None if i in self.keyorder and self.keyorder[i] is self.DUMMY: # dummy slot freeslot = i @@ -337,6 +364,17 @@ def __init__(self, *args, **kwargs): # Initialize base arguments self.update(*args, **kwargs) + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + for k, v in self.__dict__.items(): + setattr(result, k, copy.deepcopy(v, memo)) + + for key, value in super(Py27Dict, self).items(): + super(Py27Dict, result).__setitem__(copy.deepcopy(key, memo), copy.deepcopy(value, memo)) + + return result + def __reduce__(self): """ Method necessary to fully pickle Python 3 subclassed dict objects with attribute fields. diff --git a/tests/utils/test_py27hash_fix.py b/tests/utils/test_py27hash_fix.py index 55c6b0d56..2dbf6fd37 100644 --- a/tests/utils/test_py27hash_fix.py +++ b/tests/utils/test_py27hash_fix.py @@ -91,6 +91,16 @@ def test_split_with_maxsplit(self): for c in after: self.assertIsInstance(c, Py27UniStr) + def test_py27_hash(self): + a = Py27UniStr("abcdef") + self.assertEqual(a._get_py27_hash(), 484452592760221083) + # do it twice since _get_py27_hash caches the hash + self.assertEqual(a._get_py27_hash(), 484452592760221083) + + def test_deepcopy(self): + a = Py27UniStr("abcdef") + self.assert_(a is copy.deepcopy(a)) # deepcopy should give back the same object + class TestPy27LongInt(TestCase): def test_long_int(self): @@ -111,6 +121,10 @@ def test_serialized_dict_with_normal_int(self): d = {"num": i} self.assertEqual(str(d), "{'num': 100}") + def test_deepcopy(self): + a = Py27LongInt(10) + self.assert_(a is copy.deepcopy(a)) # deepcopy should give back the same object + class TestPy27Keys(TestCase): def test_merge(self): From d42d64b6a18e3457fc5f436e5a729fc863823834 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Mon, 28 Feb 2022 11:33:54 -0800 Subject: [PATCH 50/59] Update function_with_custom_code_deploy integration test case (#2320) * Update function_with_custom_code_deploy integration test case * Remove unused methods --- ...est_function_with_deployment_preference.py | 31 +++---------------- .../function_with_custom_code_deploy.json | 3 +- .../function_with_custom_code_deploy.yaml | 20 +++++++++++- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/integration/combination/test_function_with_deployment_preference.py b/integration/combination/test_function_with_deployment_preference.py index 56f0dd2aa..7783ad317 100644 --- a/integration/combination/test_function_with_deployment_preference.py +++ b/integration/combination/test_function_with_deployment_preference.py @@ -1,7 +1,7 @@ from unittest.case import skipIf from integration.helpers.base_test import BaseTest -from integration.helpers.resource import current_region_does_not_support +from integration.helpers.resource import current_region_does_not_support, generate_suffix from integration.config.service_names import CODE_DEPLOY CODEDEPLOY_APPLICATION_LOGICAL_ID = "ServerlessDeploymentApplication" @@ -16,15 +16,11 @@ def test_lambda_function_with_deployment_preference_uses_code_deploy(self): self._verify_no_deployment_then_update_and_verify_deployment() def test_lambda_function_with_custom_deployment_preference(self): - custom_deployment_config_name = "CustomLambdaDeploymentConfiguration" - # Want to delete / recreate custom deployment resource to make sure it exists and hasn't changed - if self._has_custom_deployment_configuration(custom_deployment_config_name): - self._delete_deployment_configuration(custom_deployment_config_name) + custom_deployment_config_name = "CustomLambdaDeploymentConfiguration" + generate_suffix() + parameters = [self.generate_parameter("DeployConfigName", custom_deployment_config_name)] - self._create_deployment_configuration(custom_deployment_config_name) - - self.create_and_verify_stack("combination/function_with_custom_code_deploy") - self._verify_no_deployment_then_update_and_verify_deployment() + self.create_and_verify_stack("combination/function_with_custom_code_deploy", parameters) + self._verify_no_deployment_then_update_and_verify_deployment(parameters) def test_use_default_manage_policy(self): self.create_and_verify_stack("combination/function_with_deployment_default_role_managed_policy") @@ -140,23 +136,6 @@ def _get_physical_resource_id(self, resource_type, logical_id): ) return resources_with_this_id["PhysicalResourceId"] - def _has_custom_deployment_configuration(self, deployment_name): - result = self.client_provider.code_deploy_client.list_deployment_configs()["deploymentConfigsList"] - return deployment_name in result - - def _delete_deployment_configuration(self, deployment_name): - self.client_provider.code_deploy_client.delete_deployment_config(deploymentConfigName=deployment_name) - - def _create_deployment_configuration(self, deployment_name): - client = self.client_provider.code_deploy_client - traffic_routing_config = { - "type": "TimeBasedLinear", - "timeBasedLinear": {"linearPercentage": 50, "linearInterval": 1}, - } - client.create_deployment_config( - deploymentConfigName=deployment_name, computePlatform="Lambda", trafficRoutingConfig=traffic_routing_config - ) - def _get_deployment_group_configuration_name(self, deployment_group_name, application_name): deployment_group = self.client_provider.code_deploy_client.get_deployment_group( applicationName=application_name, deploymentGroupName=deployment_group_name diff --git a/integration/resources/expected/combination/function_with_custom_code_deploy.json b/integration/resources/expected/combination/function_with_custom_code_deploy.json index 4aa5ea974..8b2a29691 100644 --- a/integration/resources/expected/combination/function_with_custom_code_deploy.json +++ b/integration/resources/expected/combination/function_with_custom_code_deploy.json @@ -5,5 +5,6 @@ { "LogicalResourceId":"MyLambdaFunctionVersion", "ResourceType":"AWS::Lambda::Version" }, { "LogicalResourceId":"ServerlessDeploymentApplication", "ResourceType":"AWS::CodeDeploy::Application" }, { "LogicalResourceId":"MyLambdaFunctionDeploymentGroup", "ResourceType":"AWS::CodeDeploy::DeploymentGroup" }, - { "LogicalResourceId":"DeploymentRole", "ResourceType":"AWS::IAM::Role" } + { "LogicalResourceId":"DeploymentRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"CustomDeploymentConfig", "ResourceType":"AWS::CodeDeploy::DeploymentConfig" } ] \ No newline at end of file diff --git a/integration/resources/templates/combination/function_with_custom_code_deploy.yaml b/integration/resources/templates/combination/function_with_custom_code_deploy.yaml index 0be5b828e..d2f9e550b 100644 --- a/integration/resources/templates/combination/function_with_custom_code_deploy.yaml +++ b/integration/resources/templates/combination/function_with_custom_code_deploy.yaml @@ -1,6 +1,11 @@ +Parameters: + DeployConfigName: + Type: String + # Just one function with a deployment preference Resources: MyLambdaFunction: + DependsOn: CustomDeploymentConfig Type: 'AWS::Serverless::Function' Properties: CodeUri: ${codeuri} @@ -10,7 +15,8 @@ Resources: AutoPublishAlias: Live DeploymentPreference: - Type: CustomLambdaDeploymentConfiguration + Type: + Ref: CustomDeploymentConfig Role: Fn::GetAtt: [ DeploymentRole, Arn ] @@ -43,3 +49,15 @@ Resources: - "lambda:InvokeFunction" - "s3:Get*" - "sns:Publish" + + CustomDeploymentConfig: + Type: AWS::CodeDeploy::DeploymentConfig + Properties: + DeploymentConfigName: + Ref: DeployConfigName + ComputePlatform: Lambda + TrafficRoutingConfig: + Type: TimeBasedLinear + TimeBasedLinear: + LinearInterval: 1 + LinearPercentage: 50 From 09c362575003a09a0c5df0fcdcad7de196d3e529 Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Mon, 28 Feb 2022 14:33:01 -0800 Subject: [PATCH 51/59] fix: Update tag count check for apigw v2 resources (#2333) --- integration/combination/test_http_api_with_cors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/combination/test_http_api_with_cors.py b/integration/combination/test_http_api_with_cors.py index 1d0f734ad..e884e82ef 100644 --- a/integration/combination/test_http_api_with_cors.py +++ b/integration/combination/test_http_api_with_cors.py @@ -34,7 +34,7 @@ def test_cors(self): # Every HttpApi should have a default tag created by SAM (httpapi:createdby: SAM) tags = api_result["Tags"] - self.assertEqual(len(tags), 1) + self.assertGreaterEqual(len(tags), 1) self.assertEqual(tags["httpapi:createdBy"], "SAM") # verifying if TimeoutInMillis is set properly in the integration From 4bf6a160e667b745f3066d479b551841604584ac Mon Sep 17 00:00:00 2001 From: Jacob Fuss <32497805+jfuss@users.noreply.github.com> Date: Tue, 1 Mar 2022 19:21:21 -0600 Subject: [PATCH 52/59] chore: Remove duplicated Docs (#2334) SAM spec is maintained on our doc site now at https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-resources-and-properties.html Since we keep the AWS Doc site up to date, we are removing the one in the repo as it is duplicative and isn't tracked as part of our releases. Co-authored-by: Jacob Fuss --- versions/2016-10-31.md | 1404 ---------------------------------------- 1 file changed, 1404 deletions(-) delete mode 100644 versions/2016-10-31.md diff --git a/versions/2016-10-31.md b/versions/2016-10-31.md deleted file mode 100644 index 9e45f1965..000000000 --- a/versions/2016-10-31.md +++ /dev/null @@ -1,1404 +0,0 @@ -# AWS Serverless Application Model (SAM) - -#### Version 2016-10-31 - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](http://www.ietf.org/rfc/rfc2119.txt). - -The AWS Serverless Application Model (SAM) is licensed under [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). - -## Table of contents -* [Introduction](#introduction) -* [Specification](#specification) - * [Format](#format) - * [Example: AWS SAM template](#example-aws-sam-template) - * [Globals Section](#globals-section) - * [Resource types](#resource-types) - * [Event source types](#event-source-types) - * [Property types](#property-types) - * [Data types](#data-types) - * [Referable properties of SAM resources](#referable-properties-of-sam-resources) - - -## Introduction -NOTE: SAM specification documentation is in process of being migrated to official [AWS SAM docs](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) page, please take a look at the [SAM specification](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification.html) there. - -[AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification.html) is a model used to define serverless applications on AWS. - -Serverless applications are applications composed of functions triggered by events. A typical serverless application consists of one or more AWS Lambda functions triggered by events such as object uploads to [Amazon S3](https://aws.amazon.com/s3), [Amazon SNS](https://aws.amazon.com/sns) notifications, and API actions. Those functions can stand alone or leverage other resources such as [Amazon DynamoDB](https://aws.amazon.com/dynamodb) tables or S3 buckets. The most basic serverless application is simply a function. - -AWS SAM is based on [AWS CloudFormation](https://aws.amazon.com/cloudformation/). A serverless application is defined in a [CloudFormation template](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html) and deployed as a [CloudFormation stack](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/updating.stacks.walkthrough.html). An AWS SAM template is a CloudFormation template. - -AWS SAM defines a set of objects which can be included in a CloudFormation template to describe common components of serverless applications easily. - - -## Specification - -### Format - -The files describing a serverless application in accordance with AWS SAM are [JSON](http://www.json.org/) or [YAML](http://yaml.org/spec/1.1/) formatted text files. These files are [CloudFormation templates](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-guide.html). - -AWS SAM introduces several new resources and property types that can be embedded into the [Resources](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html) section of the template. The templates may include all other template sections and use [CloudFormation intrinsic functions](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html) to access properties available only at runtime. - -In order to include objects defined by AWS SAM within a CloudFormation template, the template must include a `Transform` section in the document root with a value of `AWS::Serverless-2016-10-31`. - - - [Resource types](#resource-types) - - [Event source types](#event-source-types) - - [Property types](#property-types) - - [Globals Section](#globals-section) - - -### Example: AWS SAM template - -```yaml -AWSTemplateFormatVersion: '2010-09-09' -Transform: 'AWS::Serverless-2016-10-31' -Resources: - MyFunction: - Type: 'AWS::Serverless::Function' - Properties: - Handler: index.handler - Runtime: nodejs6.10 - CodeUri: 's3://my-bucket/function.zip' -``` - -All property names in AWS SAM are **case sensitive**. - -### Globals Section -Globals is a section in your SAM template to define properties common to all your Serverless Function and APIs. All the `AWS::Serverless::Function` and -`AWS::Serverless::Api` resources will inherit the properties defined here. - -Read the [Globals Guide](../docs/globals.rst) for more detailed information. - -Example: - -```yaml -Globals: - Function: - Runtime: nodejs6.10 - Timeout: 180 - Handler: index.handler - Environment: - Variables: - TABLE_NAME: data-table - Api: - EndpointConfiguration: REGIONAL - Cors: "'www.example.com'" - Domain: - DomainName: www.my-domain.com - CertificateArn: my-valid-cert-arn - EndpointConfiguration: EDGE - - SimpleTable: - SSESpecification: - SSEEnabled: true -``` - - -### Resource types - - [AWS::Serverless::Function](#awsserverlessfunction) - - [AWS::Serverless::Api](#awsserverlessapi) - - [AWS::Serverless::HttpApi](#awsserverlesshttpapi) - - [AWS::Serverless::Application](#awsserverlessapplication) - - [AWS::Serverless::SimpleTable](#awsserverlesssimpletable) - - [AWS::Serverless::LayerVersion](#awsserverlesslayerversion) - -#### AWS::Serverless::Function - -Creates a Lambda function, IAM execution role, and event source mappings which trigger the function. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Handler | `string` | **Required.** Function within your code that is called to begin execution. -Runtime | `string` | **Required.** The runtime environment. -CodeUri | `string` | [S3 Location Object](#s3-location-object) | **Either CodeUri or InlineCode must be specified.** S3 Uri or location to the function code. The S3 object this Uri references MUST be a [Lambda deployment package](http://docs.aws.amazon.com/lambda/latest/dg/deployment-package-v2.html). -InlineCode | `string` | **Either CodeUri or InlineCode must be specified.** The inline code for the lambda. -FunctionName | `string` | A name for the function. If you don't specify a name, a unique name will be generated for you. [More Info](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#cfn-lambda-function-functionname) -Description | `string` | Description of the function. -MemorySize | `integer` | Size of the memory allocated per invocation of the function in MB. Defaults to 128. -Timeout | `integer` | Maximum time that the function can run before it is killed in seconds. Defaults to 3. -Role | `string` | ARN of an IAM role to use as this function's execution role. If omitted, a default role is created for this function. -AssumeRolePolicyDocument | [IAM policy document object](http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies.html) | AssumeRolePolicyDocument of the default created role for this function. -Policies | `string` | List of `string` | [IAM policy document object](http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies.html) | List of [IAM policy document object](http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies.html) | List of [SAM Policy Templates](../docs/policy_templates.rst) | Names of AWS managed IAM policies or IAM policy documents or SAM Policy Templates that this function needs, which should be appended to the default role for this function. If the Role property is set, this property has no meaning. -PermissionsBoundary | `string` | ARN of a permissions boundary to use for this function's execution role. -Environment | [Function environment object](#environment-object) | Configuration for the runtime environment. -VpcConfig | [VPC config object](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-vpcconfig.html) | Configuration to enable this function to access private resources within your VPC. -Events | Map of `string` to [Event source object](#event-source-object) | A map (string to [Event source object](#event-source-object)) that defines the events that trigger this function. Keys are limited to alphanumeric characters. -Tags | Map of `string` to `string` | A map (string to string) that specifies the tags to be added to this function. Keys and values are limited to alphanumeric characters. Keys can be 1 to 127 Unicode characters in length and cannot be prefixed with `aws:`. Values can be 1 to 255 Unicode characters in length. When the stack is created, SAM will automatically add a `lambda:createdBy:SAM` tag to this Lambda function. Tags will also be applied to default roles generated by the function. -Tracing | `string` | String that specifies the function's [X-Ray tracing mode](http://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html). Accepted values are `Active` and `PassThrough` -KmsKeyArn | `string` | The Amazon Resource Name (ARN) of an AWS Key Management Service (AWS KMS) key that Lambda uses to encrypt and decrypt your function's environment variables. -DeadLetterQueue | `map` | [DeadLetterQueue Object](#deadletterqueue-object) | Configures SNS topic or SQS queue where Lambda sends events that it can't process. -DeploymentPreference | [DeploymentPreference Object](#deploymentpreference-object) | Settings to enable Safe Lambda Deployments. Read the [usage guide](../docs/safe_lambda_deployments.rst) for detailed information. -Layers | list of `string` | List of LayerVersion ARNs that should be used by this function. The order specified here is the order that they will be imported when running the Lambda function. -AutoPublishAlias | `string` | Name of the Alias. Read [AutoPublishAlias Guide](../docs/safe_lambda_deployments.rst#instant-traffic-shifting-using-lambda-aliases) for how it works -VersionDescription | `string` | A string that specifies the Description field which will be added on the new lambda version -ReservedConcurrentExecutions | `integer` | The maximum of concurrent executions you want to reserve for the function. For more information see [AWS Documentation on managing concurrency](https://docs.aws.amazon.com/lambda/latest/dg/concurrent-executions.html) -ProvisionedConcurrencyConfig | [ProvisionedConcurrencyConfig Object](#provisioned-concurrency-config-object) | Configure provisioned capacity for a number of concurrent executions on Lambda Alias property. -EventInvokeConfig | [EventInvokeConfig object](#event-invoke-config-object) | Configure options for [asynchronous invocation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html) on the function. -Architectures | List of `string` | The CPU architecture to run on (x86_64 or arm64), accepts only one value. Defaults to x86_64. - -##### Return values - -###### Ref - -When the logical ID of this resource is provided to the [Ref](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) intrinsic function, it returns the resource name of the underlying Lambda function. - -###### Fn::GetAtt - -When the logical ID of this resource is provided to the [Fn::GetAtt](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) intrinsic function, it returns a value for a specified attribute of this type. This section lists the available attributes. - -Attribute Name | Description ----|--- -Arn | The ARN of the Lambda function. - -###### Referencing Lambda Version & Alias resources - -When you use `AutoPublishAlias` property, SAM will generate a Lambda Version and Alias resource for you. If you want to refer to these properties in an intrinsic function such as Ref or Fn::GetAtt, you can append `.Version` or `.Alias` suffix to the function's Logical ID. SAM will convert it to the correct Logical ID of the auto-generated Version or Alias resource respectively. - -Example: - -Assume the following Serverless Function - -```yaml -Resources: - MyLambdaFunction: - Type: AWS::Serverless::Function - Properties: - ... - AutoPublishAlias: live - ... -``` - -Version can be referenced as: -```yaml -"Ref": "MyLambdaFunction.Version" -``` - -Alias can be referenced as: -```yaml -"Ref": "MyLambdaFunction.Alias" -``` - -This can be used with other intrinsic functions such as "Fn::GetAtt" or "Fn::Sub" or "Fn::Join" as well. - -###### Example: AWS::Serverless::Function - -```yaml -Handler: index.js -Runtime: nodejs6.10 -CodeUri: 's3://my-code-bucket/my-function.zip' -Description: Creates thumbnails of uploaded images -MemorySize: 1024 -Timeout: 15 -Policies: - - AWSLambdaExecute # Managed Policy - - Version: '2012-10-17' # Policy Document - Statement: - - Effect: Allow - Action: - - s3:GetObject - - s3:GetObjectACL - Resource: 'arn:aws:s3:::my-bucket/*' -Environment: - Variables: - TABLE_NAME: my-table -Events: - PhotoUpload: - Type: S3 - Properties: - Bucket: my-photo-bucket # bucket must be created in the same template -Tags: - AppNameTag: ThumbnailApp - DepartmentNameTag: ThumbnailDepartment -Layers: - - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:123456789012:layer:MyLayer:1 -``` - -#### AWS::Serverless::Api - -Creates a collection of Amazon API Gateway resources and methods that can be invoked through HTTPS endpoints. - -An `AWS::Serverless::Api` resource need not be explicitly added to a AWS Serverless Application Model template. A resource of this type is implicitly created from the union of [Api](#api) events defined on `AWS::Serverless::Function` resources defined in the template that do not refer to an `AWS::Serverless::Api` resource. An `AWS::Serverless::Api` resource should be used to define and document the API using [OpenAPI](https://github.com/OAI/OpenAPI-Specification), which provides more ability to configure the underlying Amazon API Gateway resources. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Name | `string` | A name for the API Gateway RestApi resource. -StageName | `string` | **Required** The name of the stage, which API Gateway uses as the first path segment in the invoke Uniform Resource Identifier (URI). -DefinitionUri | `string` | [S3 Location Object](#s3-location-object) | S3 URI or location to the OpenAPI document describing the API. If neither `DefinitionUri` nor `DefinitionBody` are specified, SAM will generate a `DefinitionBody` for you based on your template configuration. **Note** Intrinsic functions are not supported in external OpenAPI files, instead use DefinitionBody to define OpenAPI definition. -DefinitionBody | `JSON or YAML Object` | OpenAPI specification that describes your API. If neither `DefinitionUri` nor `DefinitionBody` are specified, SAM will generate a `DefinitionBody` for you based on your template configuration. -CacheClusterEnabled | `boolean` | Indicates whether cache clustering is enabled for the stage. -CacheClusterSize | `string` | The stage's cache cluster size. -Variables | Map of `string` to `string` | A map (string to string map) that defines the stage variables, where the variable name is the key and the variable value is the value. Variable names are limited to alphanumeric characters. Values must match the following regular expression: `[A-Za-z0-9._~:/?#&=,-]+`. -MethodSettings | List of [CloudFormation MethodSettings property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apitgateway-stage-methodsetting.html) | Configures all settings for API stage including Logging, Metrics, CacheTTL, Throttling. This value is passed through to CloudFormation. So any values supported by CloudFormation ``MethodSettings`` property can be used here. -Tags | Map of `string` to `string` | A map (string to string) that specifies the tags to be added to this API Stage. Keys and values are limited to alphanumeric characters. -EndpointConfiguration | `string` or [API EndpointConfiguration Object](#api-endpointconfiguration-object) | Specify the type of endpoint for API endpoint. Specify the type as `REGIONAL` or `EDGE`. To use a `PRIVATE` endpoint, specify a dictionary with additional [API EndpointConfiguration Object](#api-endpointconfiguration-object). (See examples in [template.yaml](../examples/2016-10-31/api_endpointconfiguration/template.yaml)) -BinaryMediaTypes | List of `string` | List of MIME types that your API could return. Use this to enable binary support for APIs. Use `~1` instead of `/` in the mime types (See examples in [template.yaml](../examples/2016-10-31/implicit_api_settings/template.yaml)). -MinimumCompressionSize | `int` | Allow compression of response bodies based on client's Accept-Encoding header. Compression is triggered when response body size is greater than or equal to your configured threshold. The maximum body size threshold is 10 MB (10,485,760 Bytes). The following compression types are supported: gzip, deflate, and identity. -Cors | `string` or [Cors Configuration](#cors-configuration) | Enable CORS for all your APIs. Specify the domain to allow as a string or specify a dictionary with additional [Cors Configuration](#cors-configuration). NOTE: Cors requires SAM to modify your OpenAPI definition. Hence it works only inline OpenAPI defined with `DefinitionBody`. -Auth | [API Auth Object](#api-auth-object) | Auth configuration for this API. Define Lambda and Cognito `Authorizers` and specify a `DefaultAuthorizer` for this API. Can specify default ApiKey restriction using `ApiKeyRequired`. Also define `ResourcePolicy` and specify `CustomStatements` which is a list of policy statements that will be added to the resource policies on the API. To whitelist specific AWS accounts, add `AwsAccountWhitelist: []` under ResourcePolicy. Similarly, `AwsAccountBlacklist`, `IpRangeWhitelist`, `IpRangeBlacklist`, `SourceVpcWhitelist`, `SourceVpcBlacklist` are also supported. -GatewayResponses | Map of [Gateway Response Type](https://docs.aws.amazon.com/apigateway/api-reference/resource/gateway-response/) to [Gateway Response Object](#gateway-response-object) | Configures Gateway Reponses for an API. Gateway Responses are responses returned by API Gateway, either directly or through the use of Lambda Authorizers. Keys for this object are passed through to Api Gateway, so any value supported by `GatewayResponse.responseType` is supported here. -AccessLogSetting | [CloudFormation AccessLogSetting property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-accesslogsetting.html) | Configures Access Log Setting for a stage. This value is passed through to CloudFormation, so any value supported by `AccessLogSetting` is supported here. -CanarySetting | [CloudFormation CanarySetting property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-stage-canarysetting.html) | Configure a Canary Setting to a Stage of a regular deployment. This value is passed through to Cloudformation, so any value supported by `CanarySetting` is supported here. -TracingEnabled | `boolean` | Indicates whether active tracing with X-Ray is enabled for the stage. -Models | `List of JSON or YAML objects` | JSON schemas that describes the models to be used by API methods. -Domain | [Domain Configuration Object](#domain-configuration-object) | Configuration settings for custom domains on API. Must contain `DomainName` and `CertificateArn` -OpenApiVersion | `string` | Version of OpenApi to use. This can either be `'2.0'` for the swagger spec or one of the OpenApi 3.0 versions, like `'3.0.1'`. Setting this property to any valid value will also remove the stage `Stage` that SAM creates. -Description | `string` | A description of the REST API resource. - -##### Return values - -###### Ref - -When the logical ID of this resource is provided to the [Ref intrinsic function](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html), it returns the resource name of the underlying API Gateway RestApi. - -##### Example: AWS::Serverless::Api - -```yaml -StageName: prod -DefinitionUri: openapi.yml -``` - -###### Referencing generated resources - Stage & Deployment - -SAM will generate an API Gateway Stage and API Gateway Deployment for every `AWS::Serverless::Api` resource. If you want to refer to these properties with the intrinsic function !Ref, you can append `.Stage` and `.Deployment` suffix to the API's Logical ID. SAM will convert it to the correct Logical ID of the auto-generated Stage or Deployment resource respectively. - -#### AWS::Serverless::HttpApi - -Creates a collection of Amazon API Gateway resources and methods that can be invoked through HTTPS endpoints. - -An `AWS::Serverless::HttpApi` resource need not be explicitly added to a AWS Serverless Application Model template. A resource of this type is implicitly created from the union of [HttpApi](#httpapi) events defined on `AWS::Serverless::Function` resources defined in the template that do not refer to an `AWS::Serverless::HttpApi` resource. An `AWS::Serverless::HttpApi` resource should be used to define and document the API using OpenApi 3.0, which provides more ability to configure the underlying Amazon API Gateway resources. - -For complete documentation about this new feature and examples, see the [HTTP API SAM Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-httpapi.html) - -##### Properties - -Property Name | Type | Description ----|:---:|--- -StageName | `string` | The name of the API stage. If a name is not given, SAM will use the `$default` stage from Api Gateway. -DefinitionUri | `string` | [S3 Location Object](#s3-location-object) | S3 URI or location to the Swagger document describing the API. If neither `DefinitionUri` nor `DefinitionBody` are specified, SAM will generate a `DefinitionBody` for you based on your template configuration. **Note** Intrinsic functions are not supported in external OpenApi files, instead use DefinitionBody to define OpenApi definition. -DefinitionBody | `JSON or YAML Object` | OpenApi specification that describes your API. If neither `DefinitionUri` nor `DefinitionBody` are specified, SAM will generate a `DefinitionBody` for you based on your template configuration. -Auth | [HTTP API Auth Object](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-httpapi-httpapiauth.html) | Configure authorization to control access to your API Gateway API. -Tags | Map of `string` to `string` | A map (string to string) that specifies the [tags](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) to be added to this HTTP API. When the stack is created, SAM will automatically add the following tag: `httpapi:createdBy: SAM`. -AccessLogSettings | [AccessLogSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-stage-accesslogsettings.html) | Settings for logging access in a stage. -CorsConfiguration | `boolean` or [CorsConfiguration Object](#cors-configuration-object) | Enable CORS for all your Http APIs. Specify `true` for adding Cors with domain '*' to your Http APIs or specify a dictionary with additional [CorsConfiguration-Object](#cors-configuration-object). SAM adds `x-amazon-apigateway-cors` header in open api definition for your Http API when this property is defined. NOTE: Cors requires SAM to modify your OpenAPI definition. Hence it works only inline OpenAPI defined with `DefinitionBody`. -DefaultRouteSettings | [RouteSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-stage-routesettings.html) | The default route settings for this HTTP API. -RouteSettings | [RouteSettings](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-stage-routesettings.html) | Per-route route settings for this HTTP API. -Domain | [Domain Configuration Object](#domain-configuration-object) | Configuration settings for custom domains on API. Must contain `DomainName` and `CertificateArn` -StageVariables | Map of `string` to `string` | A map that defines the stage variables for a Stage. Variable names can have alphanumeric and underscore characters, and the values must match [A-Za-z0-9-._~:/?#&=,]+. -FailOnWarnings | `boolean` | Specifies whether to rollback the API creation (true) or not (false) when a warning is encountered. The default value is false. -Description | `string` | A description of the HTTP API resource. - -##### Return values - -###### Ref - -When the logical ID of this resource is provided to the [Ref intrinsic function](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html), it returns the resource name of the underlying API Gateway Api. - -#### AWS::Serverless::Application - -Embeds a serverless application from the [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/) or from an Amazon S3 bucket as a nested application. Nested applications are deployed as nested stacks, which can contain multiple other resources, including other `AWS::Serverless::Application` resources. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Location | `string` or [Application Location Object](#application-location-object) | **Required** Template URL or location of nested application. If a template URL is given, it must follow the format specified in the [CloudFormation TemplateUrl documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stack.html#cfn-cloudformation-stack-templateurl) and contain a valid CloudFormation or SAM template. -Parameters | Map of `string` to `string` | Application parameter values. -NotificationARNs | List of `string` | A list of existing Amazon SNS topics where notifications about stack events are sent. -Tags | Map of `string` to `string` | A map (string to string) that specifies the [tags](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html) to be added to this application. When the stack is created, SAM will automatically add the following tags: lambda:createdBy:SAM, serverlessrepo:applicationId:\, serverlessrepo:semanticVersion:\. -TimeoutInMinutes | `integer` | The length of time, in minutes, that AWS CloudFormation waits for the nested stack to reach the CREATE_COMPLETE state. The default is no timeout. When AWS CloudFormation detects that the nested stack has reached the CREATE_COMPLETE state, it marks the nested stack resource as CREATE_COMPLETE in the parent stack and resumes creating the parent stack. If the timeout period expires before the nested stack reaches CREATE_COMPLETE, AWS CloudFormation marks the nested stack as failed and rolls back both the nested stack and parent stack. - -Other provided top-level resource attributes, e.g., Condition, DependsOn, etc, are automatically passed through to the underlying AWS::CloudFormation::Stack resource. - - -##### Return values - -###### Ref - -When the logical ID of this resource is provided to the [Ref intrinsic function](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html), it returns the resource name of the underlying CloudFormation nested stack. - -###### Fn::GetAtt - -When the logical ID of this resource is provided to the [Fn::GetAtt intrinsic function](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html), it returns a value for a specified attribute of this type. This section lists the available attributes. - -Attribute Name | Description ----|--- -Outputs.*ApplicationOutputName* | The value of the stack output with name *ApplicationOutputName*. - -##### Example: AWS::Serverless::Application - -```yaml -Resources: - MyApplication: - Type: AWS::Serverless::Application - Properties: - Location: - ApplicationId: 'arn:aws:serverlessrepo:us-east-1:012345678901:applications/my-application' - SemanticVersion: 1.0.0 - Parameters: - StringParameter: parameter-value - IntegerParameter: 2 - MyOtherApplication: - Type: AWS::Serverless::Application - Properties: - Location: https://s3.amazonaws.com/demo-bucket/template.yaml -Outputs: - MyNestedApplicationOutput: - Value: !GetAtt MyApplication.Outputs.ApplicationOutputName - Description: Example nested application output -``` - -#### AWS::Serverless::SimpleTable - -The `AWS::Serverless::SimpleTable` resource creates a DynamoDB table with a single attribute primary key. It is useful when data only needs to be accessed via a primary key. To use the more advanced functionality of DynamoDB, use an [AWS::DynamoDB::Table](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html) resource instead. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -PrimaryKey | [Primary Key Object](#primary-key-object) | Attribute name and type to be used as the table's primary key. **This cannot be modified without replacing the resource.** Defaults to `String` attribute named `id`. -ProvisionedThroughput | [Provisioned Throughput Object](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-provisionedthroughput.html) | Read and write throughput provisioning information. If ProvisionedThroughput is not specified BillingMode will be specified as PAY_PER_REQUEST -Tags | Map of `string` to `string` | A map (string to string) that specifies the tags to be added to this table. Keys and values are limited to alphanumeric characters. -TableName | `string` | Name for the DynamoDB Table -SSESpecification | [DynamoDB SSESpecification](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-table-ssespecification.html) | Specifies the settings to enable server-side encryption. - -##### Return values - -###### Ref - -When the logical ID of this resource is provided to the [Ref](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) intrinsic function, it returns the resource name of the underlying DynamoDB table. - -##### Example: AWS::Serverless::SimpleTable - -```yaml -Properties: - TableName: my-table - PrimaryKey: - Name: id - Type: String - ProvisionedThroughput: - ReadCapacityUnits: 5 - WriteCapacityUnits: 5 - Tags: - Department: Engineering - AppType: Serverless - SSESpecification: - SSEEnabled: true -``` - -#### AWS::Serverless::LayerVersion - -Creates a Lambda LayerVersion that contains library or runtime code needed by a Lambda Function. When a Serverless LayerVersion is transformed, SAM also transforms the logical id of the resource so that old LayerVersions are not automatically deleted by CloudFormation when the resource is updated. - -Property Name | Type | Description ----|:---:|--- -LayerName | `string` | Name of this layer. If you don't specify a name, the logical id of the resource will be used as the name. -Description | `string` | Description of this layer. -ContentUri | `string` | [S3 Location Object](#s3-location-object) | **Required.** S3 Uri or location for the layer code. -CompatibleArchitectures | List of `string` | List or architectures compatibles with this LayerVersion. -CompatibleRuntimes | List of `string`| List of runtimes compatible with this LayerVersion. -LicenseInfo | `string` | Information about the license for this LayerVersion. -RetentionPolicy | `string` | Options are `Retain` and `Delete`. Defaults to `Retain`. When `Retain` is set, SAM adds `DeletionPolicy: Retain` to the transformed resource so CloudFormation does not delete old versions after an update. - -##### Return values - -###### Ref - -When the logical ID of this resource is provided to the [Ref](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) intrinsic function, it returns the resource ARN of the underlying Lambda LayerVersion. - -##### Example: AWS::Serverless::LayerVersion - -```yaml -Properties: - LayerName: MyLayer - Description: Layer description - ContentUri: 's3://my-bucket/my-layer.zip' - CompatibleRuntimes: - - nodejs6.10 - - nodejs8.10 - LicenseInfo: 'Available under the MIT-0 license.' - RetentionPolicy: Retain -``` - - -### Event source types - - [S3](#s3) - - [SNS](#sns) - - [Kinesis](#kinesis) - - [MSK](#msk) - - [DynamoDB](#dynamodb) - - [SQS](#sqs) - - [Api](#api) - - [HttpApi](#httpapi) - - [Schedule](#schedule) - - [CloudWatchEvent](#cloudwatchevent) - - [EventBridgeRule](#eventbridgerule) - - [CloudWatchLogs](#cloudwatchlogs) - - [IoTRule](#iotrule) - - [AlexaSkill](#alexaskill) - - [Cognito](#cognito) - -#### S3 - -The object describing an event source with type `S3`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Bucket | `string` | **Required.** S3 bucket name. -Events | `string` | List of `string` | **Required.** See [Amazon S3 supported event types](http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types) for valid values. -Filter | [Amazon S3 notification filter](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-notificationconfiguration-config-filter.html) | Rules to filter events on. - -NOTE: To specify an S3 bucket as an event source for a Lambda function, both resources have to be declared in the same template. AWS SAM does not support specifying an existing bucket as an event source. - -##### Example: S3 event source object - -```yaml -Type: S3 -Properties: - Bucket: my-photo-bucket # bucket must be created in the same template - Events: s3:ObjectCreated:* - Filter: - S3Key: - Rules: - - Name: prefix|suffix - Value: my-prefix|my-suffix -``` - -#### SNS - -The object describing an event source with type `SNS`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Topic | `string` | **Required.** Topic ARN. -Region | `string` | Region. -FilterPolicy | [Amazon SNS filter policy](https://docs.aws.amazon.com/sns/latest/dg/message-filtering.html) | Policy assigned to the topic subscription in order to receive only a subset of the messages. -SqsSubscription | `boolean` | Set to `true` to enable batching SNS topic notifications in an SQS queue. - -##### Example: SNS event source object - -```yaml -Type: SNS -Properties: - Topic: arn:aws:sns:us-east-1:123456789012:my_topic - FilterPolicy: - store: - - example_corp - price_usd: - - numeric: - - ">=" - - 100 -``` - -#### Kinesis - -The object describing an event source with type `Kinesis`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Stream | `string` | **Required.** ARN of the Amazon Kinesis stream. -StartingPosition | `string` | **Required.** One of `TRIM_HORIZON` or `LATEST`. -BatchSize | `integer` | Maximum number of stream records to process per function invocation. -Enabled | `boolean` | Indicates whether Lambda begins polling the event source. -MaximumBatchingWindowInSeconds | `integer` | The maximum amount of time to gather records before invoking the function. -MaximumRetryAttempts | `integer` | The number of times to retry a record before it is bypassed. If an `OnFailure` destination is set, metadata describing the records will be sent to the destination. If no destination is set, the records will be bypassed -BisectBatchOnFunctionError | `boolean` | A boolean flag which determines whether a failed batch will be split in two after a failed invoke. -MaximumRecordAgeInSeconds | `integer` | The maximum age of a record that will be invoked by Lambda. If an `OnFailure` destination is set, metadata describing the records will be sent to the destination. If no destination is set, the records will be bypassed -DestinationConfig | [Destination Config Object](#destination-config-object) | Expired record metadata/retries and exhausted metadata is sent to this destination after they have passed the defined limits. -ParallelizationFactor | `integer` | Allocates multiple virtual shards, increasing the Lambda invokes by the given factor and speeding up the stream processing. -TumblingWindowInSeconds | `integer` | Tumbling window (non-overlapping time window) duration to perform aggregations. -FunctionResponseTypes | `list` | Response types enabled for your function. -FilterCriteria | [AWS Lambda Filter Criteria](#filter-criteria-object) | Configuration to filter messages from the stream before processing - -**NOTE:** `SQSSendMessagePolicy` or `SNSPublishMessagePolicy` needs to be added in `Policies` for publishing messages to the `SQS` or `SNS` resource mentioned in `OnFailure` property - - -##### Example: Kinesis event source object - -```yaml -Type: Kinesis -Properties: - Stream: arn:aws:kinesis:us-east-1:123456789012:stream/my-stream - StartingPosition: TRIM_HORIZON - BatchSize: 10 - MaximumBatchingWindowInSeconds: 10 - Enabled: true - ParallelizationFactor: 8 - MaximumRetryAttempts: 100 - BisectBatchOnFunctionError: true - MaximumRecordAgeInSeconds: 604800 - DestinationConfig: - OnFailure: - Type: SQS - Destination: !GetAtt MySqsQueue.Arn - TumblingWindowInSeconds: 0 - FunctionResponseTypes: - - ReportBatchItemFailures -``` - - -#### MSK - -The object describing an event source with type `MSK`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Stream | `string` | **Required.** ARN of the Amazon MSK stream. -StartingPosition | `string` | **Required.** One of `TRIM_HORIZON` or `LATEST`. -Topics | `list` | **Required.** List of Topics created in the Amazon MSK Stream - -##### Example: MSK event source object - -```yaml -Type: MSK -Properties: - Stream: arn:aws:kafka:us-west-2:123456789012:cluster/mycluster/6cc0432b-8618-4f44-bccc-e1fbd8fb7c4d-2 - StartingPosition: LATEST - Topics: - - "Topic1" - - "Topic2" -``` - -#### DynamoDB - -The object describing an event source with type `DynamoDB`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Stream | `string` | **Required.** ARN of the DynamoDB stream. -StartingPosition | `string` | **Required.** One of `TRIM_HORIZON` or `LATEST`. -BatchSize | `integer` | Maximum number of stream records to process per function invocation. -Enabled | `boolean` | Indicates whether Lambda begins polling the event source. -MaximumBatchingWindowInSeconds | `integer` | The maximum amount of time to gather records before invoking the function. -MaximumRetryAttempts | `integer` | The number of times to retry a record before it is bypassed. If an `OnFailure` destination is set, metadata describing the records will be sent to the destination. If no destination is set, the records will be bypassed -BisectBatchOnFunctionError | `boolean` | A boolean flag which determines whether a failed batch will be split in two after a failed invoke. -MaximumRecordAgeInSeconds | `integer` | The maximum age of a record that will be invoked by Lambda. If an `OnFailure` destination is set, metadata describing the records will be sent to the destination. If no destination is set, the records will be bypassed -DestinationConfig | [DestinationConfig Object](#destination-config-object) | Expired record metadata/retries and exhausted metadata is sent to this destination after they have passed the defined limits. -ParallelizationFactor | `integer` | Allocates multiple virtual shards, increasing the Lambda invokes by the given factor and speeding up the stream processing. -TumblingWindowInSeconds | `integer` | Tumbling window (non-overlapping time window) duration to perform aggregations. -FunctionResponseTypes | `list` | Response types enabled for your function. -FilterCriteria | [AWS Lambda Filter Criteria](#filter-criteria-object) | Configuration to filter messages from the stream before processing - -##### Example: DynamoDB event source object - -```yaml -Type: DynamoDB -Properties: - Stream: arn:aws:dynamodb:us-east-1:123456789012:table/TestTable/stream/2016-08-11T21:21:33.291 - StartingPosition: TRIM_HORIZON - BatchSize: 10 - MaximumBatchingWindowInSeconds: 10 - Enabled: false - ParallelizationFactor: 8 - MaximumRetryAttempts: 100 - BisectBatchOnFunctionError: true - MaximumRecordAgeInSeconds: 86400 - DestinationConfig: - OnFailure: - Type: SQS - Destination: !GetAtt MySqsQueue.Arn - TumblingWindowInSeconds: 0 - FunctionResponseTypes - - ReportBatchItemFailures -``` - -#### SQS - -The object describing an event source with type `SQS`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Queue | `string` | **Required.** ARN of the SQS queue. -BatchSize | `integer` | Maximum number of messages to process per function invocation. -Enabled | `boolean` | Indicates whether Lambda begins polling the event source. -FilterCriteria | [AWS Lambda Filter Criteria](#filter-criteria-object) | Configuration to filter messages from the queue before processing - -##### Example: SQS event source object - -```yaml -Type: SQS -Properties: - Queue: arn:aws:sqs:us-west-2:012345678901:my-queue - BatchSize: 10 - Enabled: false -``` - -#### Destination Config Object - -Expired record metatadata/retries exhausted metadata is sent to this destination after they have passed the defined limits. - -##### Properties -Property Name | Type | Description ----|:---:|--- -DestinationConfig | [OnFailure Object](#onfailure-object) | On failure all the messages get redirected to the given destination arn. - -#### OnFailure Object -Property Name | Type | Description ----|:---:|--- -Destination | `string` | Destination arn to redirect to either a SQS or a SNS resource -Type | `string` | This field accepts either `SQS` or `SNS` as input. This sets the required policies for sending or publishing messages to SQS or SNS resource on failure - - -##### Example -```yaml - DestinationConfig: - OnFailure: - Type: SQS # or SNS. this is optional. If this is not added then `SQSSendMessagePolicy` or `SNSPublishMessagePolicy` needs to be added in `Policies` for publishing messages to the `SQS` or `SNS` resource mentioned in `OnFailure` property - Destination: arn:aws:sqs:us-west-2:012345678901:my-queue # required -``` - -#### Api - -The object describing an event source with type `Api`. - -If an [AWS::Serverless::Api](#aws-serverless-api) resource is defined, the path and method values MUST correspond to an operation in the OpenAPI definition of the API. If no [AWS::Serverless::Api](#aws-serverless-api) is defined, the function input and output are a representation of the HTTP request and HTTP response. For example, using the JavaScript API, the status code and body of the response can be controlled by returning an object with the keys `statusCode` and `body`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Path | `string` | **Required.** Uri path for which this function is invoked. MUST start with `/`. -Method | `string` | **Required.** HTTP method for which this function is invoked. -RestApiId | `string` | Identifier of a RestApi resource which MUST contain an operation with the given path and method. Typically, this is set to [reference](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) an `AWS::Serverless::Api` resource defined in this template. If not defined, a default `AWS::Serverless::Api` resource is created using a generated Swagger document containing a union of all paths and methods defined by `Api` events defined in this template that do not specify a RestApiId. -Auth | [Function Auth Object](#function-auth-object) | Auth configuration for this specific Api+Path+Method. Useful for overriding the API's `DefaultAuthorizer` setting auth config on an individual path when no `DefaultAuthorizer` is specified or overriding the default `ApiKeyRequired` setting. -RequestModel | [Function Request Model Object](#function-request-model-object) | Request model configuration for this specific Api+Path+Method. -RequestParameters | List of `string` | List of [Function Request Parameter Object](#function-request-parameter-object) | Request parameters configuration for this specific Api+Path+Method. All parameter names must start with `method.request` and must be limited to `method.request.header`, `method.request.querystring`, or `method.request.path`. If a parameter is a `string` and NOT a [Function Request Parameter Object](#function-request-parameter-object) then `Required` and `Caching` will default to `False`. - -##### Example: Api event source object - -```yaml -Type: Api -Properties: - Path: /photos - Method: post -``` - -#### HttpApi - -The object describing an event source with type `HttpApi`. - -If an [AWS::Serverless::HttpApi](#aws-serverless-httpapi) resource is defined, the path and method values MUST correspond to an operation in the Swagger definition of the API. If no [AWS::Serverless::HttpApi](#aws-serverless-httpapi) is defined, the function input and output are a representation of the HTTP request and HTTP response. For example, using the JavaScript API, the status code and body of the response can be controlled by returning an object with the keys `statusCode` and `body`. - -See the [AWS SAM Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-httpapi.html) for full information about this feature. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Path | `string` | Uri path for which this function is invoked. MUST start with `/`. -Method | `string` | HTTP method for which this function is invoked. -ApiId | `string` | Identifier of a HttpApi resource which MUST contain an operation with the given path and method. Typically, this is set to [reference](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) an `AWS::Serverless::HttpApi` resource defined in this template. If not defined, a default `AWS::Serverless::HttpApi` resource is created using a generated OpenApi document contains a union of all paths and methods defined by `HttpApi` events defined in this template that do not specify an ApiId. -Auth | [Function Auth Object](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-httpapifunctionauth.html) | Auth configuration for this specific Api+Path+Method. Useful for overriding the API's `DefaultAuthorizer` setting auth config on an individual path when no `DefaultAuthorizer` is specified. -TimeoutInMillis | `int` | Custom timeout between 50 and 29,000 milliseconds. The default value is 5,000 milliseconds, or 5 seconds for HTTP APIs. -PayloadFormatVersion | `string` | Specify the format version of the payload sent to the Lambda HTTP API integration. If this field is not given, SAM defaults to "2.0". - -##### Example: HttpApi event source object - -```yaml -Type: HttpApi -Properties: - Path: /photos - Method: post -``` - -#### Schedule - -The object describing an event source with type `Schedule`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Schedule | `string` | **Required.** Schedule expression, which MUST follow the [schedule expression syntax rules](http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html). -Input | `string` | JSON-formatted string to pass to the function as the event body. -Name | `string` | A name for the Schedule. If you don't specify a name, a unique name will be generated. -Description | `string` | Description of Schedule. -Enabled | `boolean` | Indicated whether the Schedule is enabled. - -##### Example: Schedule event source object - -```yaml -Type: Schedule -Properties: - Schedule: rate(5 minutes) - Name: my-schedule - Description: Example schedule - Enabled: True -``` - -#### CloudWatchEvent - -The object describing an event source with type `CloudWatchEvent`. - -The CloudWatch Events service has been re-launched as Amazon EventBridge with full backwards compatibility. Please see the subsequent [EventBridgeRule](#eventbridgerule) section. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Pattern | [Event Pattern Object](http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html) | **Required.** Pattern describing which CloudWatch events trigger the function. Only matching events trigger the function. -EventBusName | `string` | The event bus to associate with this rule. If you omit this, the default event bus is used. -Input | `string` | JSON-formatted string to pass to the function as the event body. This value overrides the matched event. -InputPath | `string` | JSONPath describing the part of the event to pass to the function. - -##### Example: CloudWatchEvent event source object - -```yaml -Type: CloudWatchEvent -Properties: - Pattern: - detail: - state: - - terminated -``` - -#### EventBridgeRule - -The object describing an event source with type `EventBridgeRule`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -DeadLetterConfig | [DeadLetterConfig Object](#deadletterconfig-object) | Configure the Amazon Simple Queue Service (Amazon SQS) queue where EventBridge sends events after a failed target invocation. -Pattern | [Event Pattern Object](https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-and-event-patterns.html) | **Required.** Pattern describing which EventBridge events trigger the function. Only matching events trigger the function. -EventBusName | `string` | The event bus to associate with this rule. If you omit this, the default event bus is used. -Input | `string` | JSON-formatted string to pass to the function as the event body. This value overrides the matched event. -InputPath | `string` | JSONPath describing the part of the event to pass to the function. -RetryPolicy | [RetryPolicy Object](#retrypolicy-object) | A RetryPolicy object that includes information about the retry policy settings. -Target | [Target Object](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-target.html) | Configures the AWS resource that EventBridge invokes when a rule is triggered. - -##### Example: EventBridge event source object - -```yaml -Type: EventBridgeRule -Properties: - Pattern: - detail: - state: - - terminated - RetryPolicy: - MaximumRetryAttempts: 5 - MaximumEventAgeInSeconds: 900 - DeadLetterConfig: - Type: SQS - QueueLogicalId: EBRuleDLQ - Target: - Id: MyTarget -``` - -#### CloudWatchLogs - -The object describing an event source with type `CloudWatchLogs`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -LogGroupName | `string` | **Required.** Name of the CloudWatch Log Group from which to process logs. -FilterPattern | `string` | **Required.** A CloudWatch Logs [FilterPattern](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html) to specify which logs in the Log Group to process. - -##### Example: CloudWatchLogs event source object - -```yaml -Type: CloudWatchLogs -Properties: - LogGroupName: MyLogGroup - FilterPattern: Error -``` - -#### IoTRule - -The object describing an event source with type `IoTRule`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Sql | `string` | **Required.** The SQL statement that queries the topic. For more information, see [Rules for AWS IoT](http://docs.aws.amazon.com/iot/latest/developerguide/iot-rules.html#aws-iot-sql-reference) in the *AWS IoT Developer Guide*. -AwsIotSqlVersion | `string` | The version of the SQL rules engine to use when evaluating the rule. - -##### Example: IoTRule event source object - -```yaml -Type: IoTRule -Properties: - Sql: "SELECT * FROM 'iot/test'" -``` - -#### AlexaSkill - -The object describing an event source with type `AlexaSkill`. - -Specifying `AlexaSkill` event creates a resource policy that allows the Amazon Alexa service to call your Lambda function. To configure the Alexa service to work with your Lambda function, go to the Alexa Developer portal. - -### Property types - - [Environment object](#environment-object) - - [Event source object](#event-source-object) - - [Primary key object](#primary-key-object) - -#### Environment object - -The object describing the environment properties of a function. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Variables | Map of `string` to `string` | A map (string to string map) that defines the environment variables, where the variable name is the key and the variable value is the value. Variable names are limited to alphanumeric characters and the first character must be a letter. Values are limited to alphanumeric characters and the following special characters `_(){}[]$*+-\/"#',;.@!?`. - -##### Example: Environment object - -```yaml -Variables: - TABLE_NAME: my-table - STAGE: prod -``` - -#### Cognito - -The object describing an event source with type `Cognito`. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -UserPool | `string` | **Required.** Reference to UserPool in the same template -Trigger | `string` | List of `string` | **Required.** See [Amazon S3 supported event types](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-lambdaconfig.html) for valid values. - -NOTE: To specify a Cognito UserPool as an event source for a Lambda function, both resources have to be declared in the same template. AWS SAM does not support specifying an existing UserPool as an event source. - -##### Example: Cognito event source object - -```yaml -Type: Cognito -Properties: - UserPool: Ref: MyUserPool - Trigger: PreSignUp -``` - -#### Event source object - -The object describing the source of events which trigger the function. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Type | `string` | **Required.** Event type. Event source types include '[S3](#s3), '[SNS](#sns)', '[Kinesis](#kinesis)', '[MSK](#msk)', [DynamoDB](#dynamodb)', '[SQS](#sqs)', '[Api](#api)', '[Schedule](#schedule)', '[CloudWatchEvent](#cloudwatchevent)', '[CloudWatchLogs](#cloudwatchlogs)', '[IoTRule](#iotrule)', '[AlexaSkill](#alexaskill)'. For more information about the types, see [Event source types](#event-source-types). -Properties | * | **Required.** Object describing properties of this event mapping. Must conform to the defined `Type`. For more information about all types, see [Event source types](#event-source-types). - -##### Example: Event source object - -```yaml -Type: S3 -Properties: - Bucket: my-photo-bucket # bucket must be created in the same template -``` - -```yaml -Type: AlexaSkill -``` - -#### Provisioned Concurrency Config object - -The object describing provisioned concurrency settings on a Lambda Alias - -##### Properties -Property Name | Type | Description ----|:---:|--- -ProvisionedConcurrentExecutions | `string` | Number of concurrent executions to be provisioned for the Lambda function. Required parameter. - -#### Event Invoke Config object - -The object describing event invoke config on a Lambda function. - -```yaml - MyFunction: - Type: 'AWS::Serverless::Function' - Properties: - EventInvokeConfig: - MaximumEventAgeInSeconds: Integer (Min: 60, Max: 21600) - MaximumRetryAttempts: Integer (Min: 0, Max: 2) - DestinationConfig: - OnSuccess: - Type: [SQS | SNS | EventBridge | Function] - Destination: ARN of [SQS | SNS | EventBridge | Function] - OnFailure: - Type: [SQS | SNS | EventBridge | Function] - Destination: ARN of [SQS | SNS | EventBridge | Function] -``` - -##### Properties -Property Name | Type | Description ----|:---:|--- -MaximumEventAgeInSeconds | `integer` | The maximum age of a request that Lambda sends to a function for processing. Optional parameter. -MaximumRetryAttempts | `integer` | The maximum number of times to retry when the function returns an error. Optional parameter. -DestinationConfig | [Destination Config Object](#event-invoke-destination-config-object) | A destination for events after they have been sent to a function for processing. Optional parameter. - -#### Event Invoke Destination Config object -The object describing destination config for Event Invoke Config. - -##### Properties -Property Name | Type | Description ----|:---:|--- -OnSuccess | [Destination Config OnSuccess Object](#event-invoke-destination-config-destination-object) | A destination for events that succeeded processing. -OnFailure | [Destination Config OnFailure Object](#event-invoke-destination-config-destination-object) | A destination for events that failed processing. - -#### Event Invoke Destination Config Destination object -The object describing destination config for Event Invoke Config. - -##### Properties -Property Name | Type | Description ----|:---:|--- -Type | `string` | Type of the Resource to be invoked. Values could be [SQS | SNS | EventBridge | Lambda] -Destination | `string` | ARN of the resource to be invoked. Fn::If and Ref is supported on this property. - -The corresponding policies for the resource are generated in SAM. -Destination Property is required if Type is EventBridge and Lambda. If Type is SQS or SNS, and Destination is None, SAM auto creates these resources in the template. - -##### Generated Resources -Property Name | Type | Alias to Ref the Auto-Created Resource ----|:---:|--- -SQS | `AWS::SQS::Queue` | `.DestinationQueue` -SNS | `AWS::SNS::Topic` | `.DestinationTopic` - -#### Primary key object - -The object describing the properties of a primary key. - -##### Properties - -Property Name | Type | Description ----|:---:|--- -Name | `string` | Attribute name of the primary key. Defaults to `id`. -Type | `string` | Attribute type of the primary key. MUST be one of `String`, `Number`, or `Binary`. Defaults to `String`. - -##### Example: Primary key object - -```yaml -Properties: - PrimaryKey: - Name: id - Type: String -``` - -### Data Types - -- [S3 Location Object](#s3-location-object) -- [Application Location Object](#application-id-object) -- [DeadLetterQueue Object](#deadletterqueue-object) -- [DeadLetterConfig Object](#deadletterconfig-object) -- [RetryPolicy Object](#retrypolicy-object) -- [Cors Configuration](#cors-configuration) -- [API EndpointConfiguration Object](#api-endpointconfiguration-object) -- [API Auth Object](#api-auth-object) -- [Function Auth Object](#function-auth-object) -- [Function Request Model Object](#function-request-model-object) -- [Function Request Parameter Object](#function-request-parameter-object) -- [Gateway Response Object](#gateway-response-object) -- [CorsConfiguration Object](#cors-configuration-object) - -#### S3 Location Object - -Specifies the location of an S3 object as a dictionary containing `Bucket`, `Key`, and optional `Version` properties. - -Example: - -```yaml -CodeUri: - Bucket: mybucket-name - Key: code.zip - Version: 121212 -``` - -#### Application Location Object - -Specifies the location of an application hosted in the [AWS Serverless Application Repository](https://aws.amazon.com/serverless/serverlessrepo/) as a dictionary containing ApplicationId and SemanticVersion properties. - -Example: - -```yaml -Location: # Both parameters are required - ApplicationId: 'arn:aws:serverlessrepo:us-east-1:012345678901:applications/my-application' - SemanticVersion: 1.0.0 -``` - -#### DeadLetterQueue Object -Specifies an SQS queue or SNS topic that AWS Lambda (Lambda) sends events to when it can't process them. For more information about DLQ functionality, refer to the official documentation at http://docs.aws.amazon.com/lambda/latest/dg/dlq.html. SAM will automatically add appropriate permission to your Lambda function execution role to give Lambda service access to the resource. `sqs:SendMessage` will be added for SQS queues and `sns:Publish` for SNS topics. - -Syntax: - -```yaml -DeadLetterQueue: - Type: `SQS` or `SNS` - TargetArn: ARN of the SQS queue or SNS topic to use as DLQ. -``` - -#### DeadLetterConfig Object -The object used to specify the Amazon Simple Queue Service (Amazon SQS) queue where EventBridge sends events after a failed target invocation. Invocation can fail, for example, when sending an event to a Lambda function that doesn’t exist, or insufficient permissions to invoke the Lambda function. For more information, see [Event retry policy and using dead-letter queues](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html) in the *Amazon EventBridge User Guide*. - -**Note:** The [AWS::Serverless::Function](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html) resource type has a similar data type, `DeadLetterQueue` which handles failures that occur after successful invocation of the target Lambda function. Examples of this type of failure include Lambda throttling, or errors returned by the Lambda target function. For more information about the function `DeadLetterQueue` property, see [AWS Lambda function dead letter queues](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#invocation-dlq) in the AWS Lambda Developer Guide. - -Syntax: - -```yaml -DeadLetterConfig: - Arn: The Amazon Resource Name (ARN) of the Amazon SQS queue specified as the target for the dead-letter queue. - QueueLogicalId: The custom name of the dead letter queue that AWS SAM creates if `Type` is specified. - Type: `SQS` -``` - -#### RetryPolicy Object -A RetryPolicy object that includes information about the retry policy settings. - -Syntax: - -```yaml -MaximumEventAgeInSeconds: The maximum amount of time, in seconds, to continue to make retry attempts. -MaximumRetryAttempts: The maximum number of retry attempts to make before the request fails. Retry attempts continue until either the maximum number of attempts is made or until the duration of the MaximumEventAgeInSeconds is met. -``` - -#### DeploymentPreference Object -Specifies the configurations to enable Safe Lambda Deployments. Read the [usage guide](../docs/safe_lambda_deployments.rst) for detailed information. The following shows all available properties of this object. -TriggerConfigurations takes a list of [TriggerConfig](https://docs.aws.amazon.com/codedeploy/latest/APIReference/API_TriggerConfig.html) objects. - -```yaml -DeploymentPreference: - Enabled: True # Set to False to disable. Supports all intrinsics. - Type: Linear10PercentEvery10Minutes - Alarms: - # A list of alarms that you want to monitor - - !Ref AliasErrorMetricGreaterThanZeroAlarm - - !Ref LatestVersionErrorMetricGreaterThanZeroAlarm - Hooks: - # Validation Lambda functions that are run before & after traffic shifting - PreTraffic: !Ref PreTrafficLambdaFunction - PostTraffic: !Ref PostTrafficLambdaFunction - TriggerConfigurations: - # A list of trigger configurations you want to associate with the deployment group. Used to notify an SNS topic on - # lifecycle events. - - TriggerEvents: - # A list of events to trigger on. - - DeploymentSuccess - - DeploymentFailure - TriggerName: TestTrigger - TriggerTargetArn: !Ref MySNSTopic -``` - -#### Cors Configuration -Enable and configure CORS for the APIs. Enabling CORS will allow your API to be called from other domains. Assume your API is served from 'www.example.com' and you want to allow. - -```yaml -Cors: - AllowMethods: Optional. String containing the HTTP methods to allow. - # For example, "'GET,POST,DELETE'". If you omit this property, then SAM will automatically allow all the methods configured for each API. - # Checkout [HTTP Spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) more details on the value. - - AllowHeaders: Optional. String of headers to allow. - # For example, "'X-Forwarded-For'". Checkout [HTTP Spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) for more details on the value - - AllowOrigin: Required. String of origin to allow. - # For example, "'www.example.com'". Checkout [HTTP Spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) for more details on this value. - - MaxAge: Optional. String containing the number of seconds to cache CORS Preflight request. - # For example, "'600'" will cache request for 600 seconds. Checkout [HTTP Spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) for more details on this value - - AllowCredentials: Optional. Boolean indicating whether request is allowed to contain credentials. - # Header is omitted when false. Checkout [HTTP Spec](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) for more details on this value. -``` - -> NOTE: API Gateway requires literal values to be a quoted string, so don't forget the additional quotes in the `Allow___` values. ie. "'www.example.com'" is correct whereas "www.example.com" is wrong. - -#### API EndpointConfiguration Object - -```yaml -EndpointConfiguration: - Type: PRIVATE # OPTIONAL | Default value is REGIONAL. Accepted values are EDGE, REGIONAL, PRIVATE - VPCEndpointIds: [] # REQUIRED if Type is PRIVATE -``` - -#### API Auth Object - -Configure Auth on APIs. - - -```yaml -Auth: - ApiKeyRequired: true # OPTIONAL - UsagePlan: # OPTIONAL - CreateUsagePlan: PER_API # REQUIRED if UsagePlan property is set. accepted values: PER_API, SHARED, NONE - DefaultAuthorizer: MyCognitoAuth # OPTIONAL, if you use IAM permissions, specify AWS_IAM. - AddDefaultAuthorizerToCorsPreflight: false # OPTIONAL; Default: true - ResourcePolicy: - CustomStatements: - - Effect: Allow - Principal: * - Action: execute-api:Invoke - ... - AwsAccountWhitelist: [] - AwsAccountBlacklist: [] - IpRangeWhitelist: [] - IpRangeBlacklist: [] - SourceVpcWhitelist: [] - SourceVpcBlacklist: [] - # For AWS_IAM: - # DefaultAuthorizer: AWS_IAM - # InvokeRole: NONE # CALLER_CREDENTIALS by default unless overridden - Authorizers: [] -``` - -**Authorizers:** -Define Lambda and Cognito `Authorizers` and specify a `DefaultAuthorizer`. If you use IAM permission, only specify `AWS_IAM` to a `DefaultAuthorizer`. For more information, see the documentation on [Lambda Authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) and [Amazon Cognito User Pool Authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html) and [IAM Permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/permissions.html). - -```yaml -Auth: - Authorizers: - MyCognitoAuth: - UserPoolArn: !GetAtt MyCognitoUserPool.Arn # Can also accept an array - AuthorizationScopes: - - scope1 # List of authorization scopes - Identity: # OPTIONAL - Header: MyAuthorizationHeader # OPTIONAL; Default: 'Authorization' - ValidationExpression: myauthvalidationexpression # OPTIONAL - - MyLambdaTokenAuth: - FunctionPayloadType: TOKEN # OPTIONAL; Defaults to 'TOKEN' when `FunctionArn` is specified - FunctionArn: !GetAtt MyAuthFunction.Arn - FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access # OPTIONAL - Identity: - Header: MyCustomAuthHeader # OPTIONAL; Default: 'Authorization' - ValidationExpression: mycustomauthexpression # OPTIONAL - ReauthorizeEvery: 20 # OPTIONAL; Service Default: 300 - - MyLambdaRequestAuth: - FunctionPayloadType: REQUEST - FunctionArn: !GetAtt MyAuthFunction.Arn - FunctionInvokeRole: arn:aws:iam::123456789012:role/S3Access # OPTIONAL - Identity: - # Must specify at least one of Headers, QueryStrings, StageVariables, or Context - Headers: # OPTIONAL - - Authorization1 - QueryStrings: # OPTIONAL - - Authorization2 - StageVariables: # OPTIONAL - - Authorization3 - Context: # OPTIONAL - - Authorization4 - ReauthorizeEvery: 0 # OPTIONAL; Service Default: 300 -``` - -**ApiKey:** Configure ApiKey restriction for all methods and paths on an API. This setting can be overriden on individual `AWS::Serverless::Function` using the [Function Auth Object](#function-auth-object). Typically this would be used to require ApiKey on all methods and then override it on select methods that you want to be public. - -```yaml -Auth: - ApiKeyRequired: true -``` - -**ResourcePolicy:** -Configure Resource Policy for all methods and paths on an API. This setting can also be defined on individual `AWS::Serverless::Function` using the [Function Auth Object](#function-auth-object). This is required for APIs with `EndpointConfiguration: PRIVATE`. - - -```yaml -Auth: - ResourcePolicy: - CustomStatements: # Supports Ref and Fn::If conditions, does not work with AWS::NoValue in policy statements - - Effect: Allow - Principal: * - Action: execute-api:Invoke - ... - AwsAccountWhitelist: [] # Supports Ref - AwsAccountBlacklist: [] # Supports Ref - IpRangeWhitelist: [] # Supports Ref - IpRangeBlacklist: [] # Supports Ref - SourceVpcWhitelist: [] # Supports Ref - SourceVpcBlacklist: [] # Supports Ref - -``` - -**UsagePlan:** -Create Usage Plan for API Auth. Usage Plans can be set in Globals level as well for RestApis. -SAM creates a single Usage Plan, Api Key and Usage Plan Api Key resources if `CreateUsagePlan` is `SHARED` and a Usage Plan, Api Key and Usage Plan Api Key resources per Api when `CreateUsagePlan` is `PER_API`. - -```yaml - Auth: - UsagePlan: - CreateUsagePlan: PER_API # Required supported values: SHARED | NONE | PER_API -``` -#### Function Auth Object - -Configure Auth for a specific Api+Path+Method. - -```yaml -Auth: - Authorizer: MyCognitoAuth # OPTIONAL, if you use IAM permissions in each functions, specify AWS_IAM. - AuthorizationScopes: # OPTIONAL - - scope1 - - scope2 -``` - -If you have specified a Global Authorizer on the API and want to make a specific Function public, override with the following: - -```yaml -Auth: - Authorizer: 'NONE' -``` - -Require api keys for a specific Api+Path+Method. - -```yaml -Auth: - ApiKeyRequired: true -``` - -If you have specified `ApiKeyRequired: true` globally on the API and want to make a specific Function public, override with the following: - -```yaml -Auth: - ApiKeyRequired: false -``` - -#### Function Request Model Object - -Configure Request Model for a specific Api+Path+Method. - -```yaml -RequestModel: - Model: User # REQUIRED; must match the name of a model defined in the Models property of the AWS::Serverless::API - Required: true # OPTIONAL; boolean -``` - -#### Function Request Parameter Object - -Configure Request Parameter for a specific Api+Path+Method. - -```yaml -- method.request.header.Authorization: - Required: true - Caching: true -``` - -#### Gateway Response Object - -Configure Gateway Responses on APIs. These are associated with the ID of a Gateway Response [response type][]. -For more information, see the documentation on [`AWS::ApiGateway::GatewayResponse`][]. - -```yaml -GatewayResponses: - UNAUTHORIZED: - StatusCode: 401 # Even though this is the default value for UNAUTHORIZED. - ResponseTemplates: - "application/json": '{ "message": $context.error.messageString }' - ResponseParameters: - Paths: - path-key: "'value'" - QueryStrings: - query-string-key: "'value'" - Headers: - Access-Control-Expose-Headers: "'WWW-Authenticate'" - Access-Control-Allow-Origin: "'*'" - WWW-Authenticate: >- - 'Bearer realm="admin"' -``` - -All properties of a Gateway Response object are optional. API Gateway has knowledge of default status codes to associate with Gateway Responses, so – for example – `StatusCode` is only used in order to override this value. - -> NOTE: API Gateway spec allows values under the `ResponseParameters` and `ResponseTemplates` properties to be templates. In order to send constant values, don't forget the additional quotes. ie. "'WWW-Authenticate'" is correct whereas "WWW-Authenticate" is wrong. - -[response type]: https://docs.aws.amazon.com/apigateway/api-reference/resource/gateway-response/ -[`AWS::ApiGateway::GatewayResponse`]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-gatewayresponse.html - -### Domain Configuration object -Enable custom domains to be configured with your Api. Currently only supports Creating Api gateway resources for custom domains. - -```yaml -Domain: - DomainName: String # REQUIRED | custom domain name being configured on the api, "www.example.com" - CertificateArn: String # REQUIRED | Must be a valid [certificate ARN](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-certificatemanager-certificate.html), and for EDGE endpoint configuration the certificate must be in us-east-1 - EndpointConfiguration: "EDGE" # optional | Default value is REGIONAL | Accepted values are EDGE | REGIONAL - SecurityPolicy: "TLS_1_2" # optional | Default value is TLS_1_0 | Accepted values are TLS_1_0| TLS_1_2 - BasePath: - - String # optional | Default value is '/' | List of basepaths to be configured with the ApiGateway Domain Name - Route53: # optional | Default behavior is to treat as None - does not create Route53 resources | Enable these settings to create Route53 Recordsets - HostedZoneId: String # ONE OF `HostedZoneId`, `HostedZoneName` REQUIRED | Must be a hostedzoneid value of a [`AWS::Route53::HostedZone`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-hostedzone.html) resource - HostedZoneName: String # ONE OF `HostedZoneId`, `HostedZoneName` REQUIRED | Must be the `Name` of an [`AWS::Route53::HostedZone`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-hostedzone.html) resource - EvaluateTargetHealth: Boolean # optional | default value is false - DistributionDomainName: String # OPTIONAL if the EndpointConfiguration is EDGE | Default points to Api Gateway Distribution | Domain name of a [cloudfront distribution](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-distribution.html) - IpV6: Boolean # optional | default value is false -``` - -#### Cors Configuration Object -Enable and configure CORS for the HttpAPIs. Enabling CORS will allow your Http API to be called from other domains. -It set to `true` SAM adds '*' for the allowed origins. -When CorsConfiguration is set at property level and also in OpenApi, SAM merges them by overriding the header values in OpenApi with the `CorsConfiguration` property values -When intrinsic functions are used either set the CORS configuration as a property or define CORS in OpenApi definition. -Checkout [HTTPAPI Gateway Developer guide](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html) for more details on these values -```yaml -CorsConfiguration: - AllowMethods: Optional. List containing the HTTP methods to allow for the HttpApi. - AllowHeaders: Optional. List of headers to allow. - AllowOrigins: Optional. List of origins to allow. - MaxAge: Optional. Integer containing the number of seconds to cache CORS Preflight request. - # For example, 600 will cache request for 600 seconds. - AllowCredentials: Optional. Boolean indicating whether request is allowed to contain credentials. - ExposeHeaders: Optional. List of allowed headers -``` - -##### Example -```yaml - CorsConfiguration: #true - AllowHeaders: - - "*" - AllowMethods: - - "GET" - AllowOrigins: - - "https://www.example.com" - ExposeHeaders: - - "*" -``` - -### Referable properties of SAM resources -- [AWS::Serverless::Function](#referable-properties-of-serverless-function) -- [AWS::Serverless::Api](#referable-properties-of-serverless-RestApi) -- [AWS::Serverless::HttpApi](#referable-properties-of-serverless-HttpApi) - -#### Referable properties of Serverless Function -Property Name | Reference | LogicalId | Description ----|:---:|---|--- -Alias | `function-logical-id`.Alias | `function-logical-id`Alias`alias-name` | SAM generates an `AWS::Lambda::Alias` resource when `AutoPublishAlias` property is set. This resource can be referenced in intrinsic functions by using the resource logical ID or `function-logical-id`.Alias -Version | `function-logical-id`.Version | `function-logical-id`Version`sha` | SAM generates an `AWS::Lambda::Version` resource when `AutoPublishAlias` property is set. This resource can be referenced in intrinsic functions by using the resource logical ID or `function-logical-id`.Version -DestinationTopic | `function-logical-id`.DestinationTopic |`function-logical-id`EventInvokeConfig`OnSuccess/OnFailure`Topic| SAM auto creates an `AWS::SNS::Topic` resource when `Destination` property of `DestinationConfig` property in `EventInvokeConfig` property is not specified. This generated resource can be referenced by using `function-logical-id`.DestinationTopic -DestinationQueue | `function-logical-id`.DestinationQueue |`function-logical-id`EventInvokeConfig`OnSuccess/OnFailure`Queue | SAM auto creates an `AWS::SQS::Queue` resource when `Destination` property of `DestinationConfig` property in `EventInvokeConfig` property is not specified. This generated resource can be referenced by using `function-logical-id`.DestinationQueue - -#### Referable properties of Serverless RestApi - -Property Name | Reference | LogicalId | Description ----|:---:|---|--- -Stage | `restapi-logical-id`.Stage | `restapi-logical-id` `StageName`Stage | SAM generates `AWS::ApiGateway::Stage` resource when `AWS::Serverless::Api` resource is defined. This resource can be referenced in intrinsic function using the resource logical id or `restapi-logical-id`.Stage -Deployment | `restapi-logical-id`.Deployment | `restapi-logical-id`Deployment`sha` | SAM generates `AWS::ApiGateway::Deployment` resource when `AWS::Serverless::Api` resource is defined. This resource can be referenced in intrinsic function using the resource logical id or `restapi-logical-id`.Deployment -DomainName | `restapi-logical-id`.DomainName | `domain-logical-id` | `AWS::ApiGateway::DomainName` resource can be referenced by using the resource logical id or `restapi-logical-id`.DomainName when `DomainName` resource is defined in `Domain` property of `AWS::Serverless::Api` -UsagePlan | `restapi-logical-id`.UsagePlan | `restapi-logical-id`UsagePlan | SAM generates UsagePlan, UsagePlanKey and ApiKey resources when `UsagePlan` property is set. UsagePlan resource can be referenced in intrinsic function using the resource logical id or `restapi-logical-id`.UsagePlan -UsagePlanKey | `restapi-logical-id`.UsagePlanKey |`restapi-logical-id`UsagePlanKey | SAM generates UsagePlan, UsagePlanKey and ApiKey resources when `UsagePlan` property is set. UsagePlanKey resource can be referenced in intrinsic function using the resource logical id or `restapi-logical-id`.UsagePlanKey -ApiKey | `restapi-logical-id`.ApiKey |`restapi-logical-id`ApiKey | SAM generates UsagePlan, UsagePlanKey and ApiKey resources when `UsagePlan` property is set. ApiKey resource can be referenced in intrinsic function using the resource logical id or `restapi-logical-id`.ApiKey - -#### Referable properties of Serverless HttpApi - -Property Name | Reference | LogicalId | Description ----|:---:|---|--- -Stage | `httpapi-logical-id`.Stage | `httpapi-logical-id`ApiGatewayDefaultStage or `httpapi-logical-id` `StageName`Stage | SAM generates `AWS::ApiGatewayV2::Stage` resource with `httpapi-logical-id`ApiGatewayDefaultStage logical id if `StageName` property is not defined. If an explicit `StageName` property is defined them SAM generates `AWS::ApiGatewayV2::Stage` resource with `httpapi-logical-id` `StageName`Stage logicalId. This resource can be referenced in intrinsic functions using `httpapi-logical-id`.Stage -DomainName | `httpapi-logical-id`.DomainName | `domain-logical-id` | `AWS::ApiGatewayV2::DomainName` resource can be referenced by using the resource logical id or `restapi-logical-id`.DomainName when `DomainName` resource is defined in `Domain` property of `AWS::Serverless::Api` From 802d006549f6c349e12b871ccf8ee1f3898ebca5 Mon Sep 17 00:00:00 2001 From: jonife <79116465+jonife@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:24:30 -0600 Subject: [PATCH 53/59] validate Lambda Authorizer property identity (#2322) --- samtranslator/model/apigatewayv2.py | 5 ++ ..._property_indentity_with_invalid_type.yaml | 51 +++++++++++++++++++ ..._property_indentity_with_invalid_type.json | 8 +++ 3 files changed, 64 insertions(+) create mode 100644 tests/translator/input/error_api_authorizer_property_indentity_with_invalid_type.yaml create mode 100644 tests/translator/output/error_api_authorizer_property_indentity_with_invalid_type.json diff --git a/samtranslator/model/apigatewayv2.py b/samtranslator/model/apigatewayv2.py index c7c1ea9be..a9976a1be 100644 --- a/samtranslator/model/apigatewayv2.py +++ b/samtranslator/model/apigatewayv2.py @@ -168,6 +168,11 @@ def _validate_lambda_authorizer(self): self.api_logical_id, self.name + " Lambda Authorizer must define 'AuthorizerPayloadFormatVersion'." ) + if self.identity and not isinstance(self.identity, dict): + raise InvalidResourceException( + self.api_logical_id, self.name + " Lambda Authorizer property 'identity' is of invalid type." + ) + def generate_openapi(self): """ Generates OAS for the securitySchemes section diff --git a/tests/translator/input/error_api_authorizer_property_indentity_with_invalid_type.yaml b/tests/translator/input/error_api_authorizer_property_indentity_with_invalid_type.yaml new file mode 100644 index 000000000..15c75de04 --- /dev/null +++ b/tests/translator/input/error_api_authorizer_property_indentity_with_invalid_type.yaml @@ -0,0 +1,51 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: python3.7 + InlineCode: | + def handler(event, context): + return {'body': 'Hello World!', 'statusCode': 200} + MemorySize: 128 + Events: + PostApi: + Type: HttpApi + Properties: + Auth: + Authorizer: MyLambdaAuthUpdated + ApiId: + Ref: MyApi + Method: POST + Path: /post + + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + print("hello") + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + Tags: + Tag1: value1 + Tag2: value2 + Auth: + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: LambdaAuthorizationIdentity + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + + \ No newline at end of file diff --git a/tests/translator/output/error_api_authorizer_property_indentity_with_invalid_type.json b/tests/translator/output/error_api_authorizer_property_indentity_with_invalid_type.json new file mode 100644 index 000000000..2b13c15b2 --- /dev/null +++ b/tests/translator/output/error_api_authorizer_property_indentity_with_invalid_type.json @@ -0,0 +1,8 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyApi] is invalid. MyLambdaAuthUpdated Lambda Authorizer property 'identity' is of invalid type.", + "errors": [ + { + "errorMessage": "Resource with id [MyApi] is invalid. MyLambdaAuthUpdated Lambda Authorizer property 'identity' is of invalid type." + } + ] +} \ No newline at end of file From 7bd24e574bcfeb4d37f7dfd1dfc55820910c33d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 9 Mar 2022 18:49:49 +0100 Subject: [PATCH 54/59] chore: update setup.cfg key to use underscore (#2051) Update 'description-file' to 'description_file', in order to silence setuptools deprecation warning: /usr/lib/python3.9/site-packages/setuptools/dist.py:691: UserWarning: Usage of dash-separated 'description-file' will not be supported in future versions. Please use the underscore name 'description_file' instead Co-authored-by: Jacob Fuss <32497805+jfuss@users.noreply.github.com> --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 52a96f1e6..23d44b7a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] -description-file = README.md +description_file = README.md license_files = LICENSE* NOTICE* - THIRD_PARTY_LICENSES \ No newline at end of file + THIRD_PARTY_LICENSES From b2f24d1de73e234fd0c5dbe3d310b055fb22f15a Mon Sep 17 00:00:00 2001 From: Daniel Mil <84205762+mildaniel@users.noreply.github.com> Date: Wed, 9 Mar 2022 11:03:35 -0800 Subject: [PATCH 55/59] test: Add retries on flaky integration test (#2343) * Add retries on flaky integration test * Fix cors origins header name --- .../combination/test_api_with_gateway_responses.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/integration/combination/test_api_with_gateway_responses.py b/integration/combination/test_api_with_gateway_responses.py index 3d77a51b3..c3efdc10f 100644 --- a/integration/combination/test_api_with_gateway_responses.py +++ b/integration/combination/test_api_with_gateway_responses.py @@ -1,6 +1,7 @@ from unittest.case import skipIf from integration.helpers.base_test import BaseTest +from integration.helpers.deployer.utils.retry import retry from integration.helpers.resource import current_region_does_not_support from integration.config.service_names import GATEWAY_RESPONSES @@ -30,8 +31,12 @@ def test_gateway_responses(self): self.assertEqual(gateway_response.get("statusCode"), None, "gatewayResponse: status code must be none") base_url = stack_outputs["ApiUrl"] - response = self.verify_get_request_response(base_url + "iam", 403) - access_control_allow_origin = response.headers["Access-Control-Allow-Origin"] + self._verify_request_response_and_cors(base_url + "iam", 403) + + @retry(AssertionError, exc_raise=AssertionError, exc_raise_msg="Unable to verify GatewayResponse request.") + def _verify_request_response_and_cors(self, url, expected_response): + response = self.verify_get_request_response(url, expected_response) + access_control_allow_origin = response.headers.get("Access-Control-Allow-Origin", "") self.assertEqual(access_control_allow_origin, "*", "Access-Control-Allow-Origin must be '*'") From d796f3023ef4bde5510f3870c2fcc3c17b6d1328 Mon Sep 17 00:00:00 2001 From: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Date: Thu, 10 Mar 2022 08:19:43 -0800 Subject: [PATCH 56/59] chore: remove py3.6 support and add py3.9 and py3.10 (#2311) --- appveyor-integration-test.yml | 6 ++++-- appveyor.yml | 6 ++++-- setup.py | 4 +++- tox.ini | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/appveyor-integration-test.yml b/appveyor-integration-test.yml index 68c4ee7a9..1e7b9add9 100644 --- a/appveyor-integration-test.yml +++ b/appveyor-integration-test.yml @@ -3,12 +3,14 @@ image: Ubuntu environment: matrix: - - TOXENV: py36 - PYTHON_VERSION: '3.6' - TOXENV: py37 PYTHON_VERSION: '3.7' - TOXENV: py38 PYTHON_VERSION: '3.8' + - TOXENV: py39 + PYTHON_VERSION: '3.9' + - TOXENV: py310 + PYTHON_VERSION: '3.10' build: off diff --git a/appveyor.yml b/appveyor.yml index 1552fc748..8373ed87f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,12 +3,14 @@ image: Ubuntu environment: matrix: - - TOXENV: py36 - PYTHON_VERSION: '3.6' - TOXENV: py37 PYTHON_VERSION: '3.7' - TOXENV: py38 PYTHON_VERSION: '3.8' + - TOXENV: py39 + PYTHON_VERSION: '3.9' + - TOXENV: py310 + PYTHON_VERSION: '3.10' build: off diff --git a/setup.py b/setup.py index 2d304b1a4..7c4fcd876 100755 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ def read_requirements(req="base.txt"): "NOTICE", "THIRD_PARTY_LICENSES", ), + python_requires=">=3.7, <=4.0, !=4.0", install_requires=read_requirements("base.txt"), include_package_data=True, extras_require={"dev": read_requirements("dev.txt")}, @@ -79,9 +80,10 @@ def read_requirements(req="base.txt"): "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: Internet", "Topic :: Software Development :: Build Tools", "Topic :: Utilities", diff --git a/tox.ini b/tox.ini index 1c3e6577e..5cb1bcce6 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py36, py37, py38 +envlist = py37, py38, py39, py310 [testenv] commands = make pr From 4f7649e9e6ea7f56e6944770fb8c06d31b1d14e7 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Mon, 14 Mar 2022 13:28:49 -0700 Subject: [PATCH 57/59] Revert "chore: bump version to 1.43.0 (#2276)" (#2345) This reverts commit ab6943a340a3f489af62b8c70c1366242b2887fe. --- samtranslator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index eb012bb42..5d0a428bc 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.43.0" +__version__ = "1.42.0" From efef98978178eb774100e047f2a6cb8efe560507 Mon Sep 17 00:00:00 2001 From: Ruperto Torres <86501267+torresxb1@users.noreply.github.com> Date: Mon, 14 Mar 2022 16:50:51 -0700 Subject: [PATCH 58/59] Revert "handle 'Invalid Swagger Document' and refactor some validation into Swagger Editor constructor (#2263)" (#2346) This reverts commit 59df2db23d5ed4a2195bf9e563d1d2a648b1bac1. --- samtranslator/model/api/api_generator.py | 80 +++++++++++++++---- samtranslator/swagger/swagger.py | 44 +++------- tests/model/api/test_api_generator.py | 4 +- tests/swagger/test_swagger.py | 8 +- .../input/api_with_resource_refs.yaml | 4 +- ...finition_body_invalid_openapi_version.yaml | 13 --- ...ror_api_definition_body_invalid_paths.yaml | 11 --- ...ition_body_missing_openapi_or_swagger.yaml | 12 --- ...ror_api_definition_body_missing_paths.yaml | 10 --- .../input/error_api_invalid_auth.yaml | 8 ++ .../error_api_invalid_definitionuri.yaml | 4 +- .../error_api_invalid_request_model.yaml | 12 +++ tests/translator/input/explicit_api.yaml | 4 +- .../input/explicit_api_openapi_3.yaml | 4 +- .../output/api_with_resource_refs.json | 12 +-- .../output/aws-cn/api_with_resource_refs.json | 12 +-- .../output/aws-cn/explicit_api.json | 10 +-- .../output/aws-cn/explicit_api_openapi_3.json | 10 +-- .../aws-us-gov/api_with_resource_refs.json | 12 +-- .../output/aws-us-gov/explicit_api.json | 10 +-- .../aws-us-gov/explicit_api_openapi_3.json | 10 +-- ...finition_body_invalid_openapi_version.json | 1 - ...ror_api_definition_body_invalid_paths.json | 1 - ...ition_body_missing_openapi_or_swagger.json | 1 - ...ror_api_definition_body_missing_paths.json | 1 - .../output/error_api_invalid_auth.json | 2 +- .../error_api_invalid_request_model.json | 2 +- tests/translator/output/explicit_api.json | 10 +-- .../output/explicit_api_openapi_3.json | 10 +-- tests/translator/test_translator.py | 3 +- 30 files changed, 159 insertions(+), 166 deletions(-) delete mode 100644 tests/translator/input/error_api_definition_body_invalid_openapi_version.yaml delete mode 100644 tests/translator/input/error_api_definition_body_invalid_paths.yaml delete mode 100644 tests/translator/input/error_api_definition_body_missing_openapi_or_swagger.yaml delete mode 100644 tests/translator/input/error_api_definition_body_missing_paths.yaml delete mode 100644 tests/translator/output/error_api_definition_body_invalid_openapi_version.json delete mode 100644 tests/translator/output/error_api_definition_body_invalid_paths.json delete mode 100644 tests/translator/output/error_api_definition_body_missing_openapi_or_swagger.json delete mode 100644 tests/translator/output/error_api_definition_body_missing_paths.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 73e4b30dc..806c55450 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -236,8 +236,6 @@ def __init__( self.template_conditions = template_conditions self.mode = mode - self.swagger_editor = SwaggerEditor(self.definition_body) if self.definition_body else None - def _construct_rest_api(self): """Constructs and returns the ApiGateway RestApi. @@ -284,7 +282,7 @@ def _construct_rest_api(self): rest_api.BodyS3Location = self._construct_body_s3_dict() elif self.definition_body: # # Post Process OpenApi Auth Settings - self.definition_body = self._openapi_postprocess(self.swagger_editor.swagger) + self.definition_body = self._openapi_postprocess(self.definition_body) rest_api.Body = self.definition_body if self.name: @@ -310,7 +308,9 @@ def _add_endpoint_extension(self): raise InvalidResourceException( self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property." ) - self.swagger_editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint) + editor = SwaggerEditor(self.definition_body) + editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint) + self.definition_body = editor.swagger def _construct_body_s3_dict(self): """Constructs the RestApi's `BodyS3Location property`_, from the SAM Api's DefinitionUri property. @@ -626,6 +626,13 @@ def _add_cors(self): else: raise InvalidResourceException(self.logical_id, INVALID_ERROR) + if not SwaggerEditor.is_valid(self.definition_body): + raise InvalidResourceException( + self.logical_id, + "Unable to add Cors configuration because " + "'DefinitionBody' does not contain a valid Swagger definition.", + ) + if properties.AllowCredentials is True and properties.AllowOrigin == _CORS_WILDCARD: raise InvalidResourceException( self.logical_id, @@ -634,9 +641,10 @@ def _add_cors(self): "'AllowOrigin' is \"'*'\" or not set", ) - for path in self.swagger_editor.iter_on_path(): + editor = SwaggerEditor(self.definition_body) + for path in editor.iter_on_path(): try: - self.swagger_editor.add_cors( + editor.add_cors( path, properties.AllowOrigin, properties.AllowHeaders, @@ -647,6 +655,9 @@ def _add_cors(self): except InvalidTemplateException as ex: raise InvalidResourceException(self.logical_id, ex.message) + # Assign the Swagger back to template + self.definition_body = editor.swagger + def _add_binary_media_types(self): """ Add binary media types to Swagger @@ -659,7 +670,11 @@ def _add_binary_media_types(self): if self.binary_media and not self.definition_body: return - self.swagger_editor.add_binary_media_types(self.binary_media) + editor = SwaggerEditor(self.definition_body) + editor.add_binary_media_types(self.binary_media) + + # Assign the Swagger back to template + self.definition_body = editor.swagger def _add_auth(self): """ @@ -678,13 +693,20 @@ def _add_auth(self): if not all(key in AuthProperties._fields for key in self.auth.keys()): raise InvalidResourceException(self.logical_id, "Invalid value for 'Auth' property") + if not SwaggerEditor.is_valid(self.definition_body): + raise InvalidResourceException( + self.logical_id, + "Unable to add Auth configuration because " + "'DefinitionBody' does not contain a valid Swagger definition.", + ) + swagger_editor = SwaggerEditor(self.definition_body) auth_properties = AuthProperties(**self.auth) authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer) if authorizers: - self.swagger_editor.add_authorizers_security_definitions(authorizers) + swagger_editor.add_authorizers_security_definitions(authorizers) self._set_default_authorizer( - self.swagger_editor, + swagger_editor, authorizers, auth_properties.DefaultAuthorizer, auth_properties.AddDefaultAuthorizerToCorsPreflight, @@ -692,17 +714,19 @@ def _add_auth(self): ) if auth_properties.ApiKeyRequired: - self.swagger_editor.add_apikey_security_definition() - self._set_default_apikey_required(self.swagger_editor) + swagger_editor.add_apikey_security_definition() + self._set_default_apikey_required(swagger_editor) if auth_properties.ResourcePolicy: SwaggerEditor.validate_is_dict( auth_properties.ResourcePolicy, "ResourcePolicy must be a map (ResourcePolicyStatement)." ) - for path in self.swagger_editor.iter_on_path(): - self.swagger_editor.add_resource_policy(auth_properties.ResourcePolicy, path, self.stage_name) + for path in swagger_editor.iter_on_path(): + swagger_editor.add_resource_policy(auth_properties.ResourcePolicy, path, self.stage_name) if auth_properties.ResourcePolicy.get("CustomStatements"): - self.swagger_editor.add_custom_statements(auth_properties.ResourcePolicy.get("CustomStatements")) + swagger_editor.add_custom_statements(auth_properties.ResourcePolicy.get("CustomStatements")) + + self.definition_body = self._openapi_postprocess(swagger_editor.swagger) def _construct_usage_plan(self, rest_api_stage=None): """Constructs and returns the ApiGateway UsagePlan, ApiGateway UsagePlanKey, ApiGateway ApiKey for Auth. @@ -905,6 +929,15 @@ def _add_gateway_responses(self): ), ) + if not SwaggerEditor.is_valid(self.definition_body): + raise InvalidResourceException( + self.logical_id, + "Unable to add Auth configuration because " + "'DefinitionBody' does not contain a valid Swagger definition.", + ) + + swagger_editor = SwaggerEditor(self.definition_body) + # The dicts below will eventually become part of swagger/openapi definition, thus requires using Py27Dict() gateway_responses = Py27Dict() for response_type, response in self.gateway_responses.items(): @@ -916,7 +949,10 @@ def _add_gateway_responses(self): ) if gateway_responses: - self.swagger_editor.add_gateway_responses(gateway_responses) + swagger_editor.add_gateway_responses(gateway_responses) + + # Assign the Swagger back to template + self.definition_body = swagger_editor.swagger def _add_models(self): """ @@ -932,10 +968,22 @@ def _add_models(self): self.logical_id, "Models works only with inline Swagger specified in " "'DefinitionBody' property." ) + if not SwaggerEditor.is_valid(self.definition_body): + raise InvalidResourceException( + self.logical_id, + "Unable to add Models definitions because " + "'DefinitionBody' does not contain a valid Swagger definition.", + ) + if not all(isinstance(model, dict) for model in self.models.values()): raise InvalidResourceException(self.logical_id, "Invalid value for 'Models' property") - self.swagger_editor.add_models(self.models) + swagger_editor = SwaggerEditor(self.definition_body) + swagger_editor.add_models(self.models) + + # Assign the Swagger back to template + + self.definition_body = self._openapi_postprocess(swagger_editor.swagger) def _openapi_postprocess(self, definition_body): """ diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index e8f57fc3b..54321db07 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -45,13 +45,19 @@ def __init__(self, doc): modifications on this copy. :param dict doc: Swagger document as a dictionary - :raises InvalidDocumentException if doc is invalid + :raises ValueError: If the input Swagger document does not meet the basic Swagger requirements. """ + if not SwaggerEditor.is_valid(doc): + raise ValueError("Invalid Swagger document") + self._doc = copy.deepcopy(doc) - self.validate_definition_body(doc) + self.paths = self._doc["paths"] + self.security_definitions = self._doc.get("securityDefinitions", Py27Dict()) + self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, Py27Dict()) + self.resource_policy = self._doc.get(self._X_APIGW_POLICY, Py27Dict()) + self.definitions = self._doc.get("definitions", Py27Dict()) - self.paths = self._doc.get("paths") # https://swagger.io/specification/#path-item-object # According to swagger spec, # each path item object must be a dict (even it is empty). @@ -61,11 +67,6 @@ def __init__(self, doc): for path_item in self.get_conditional_contents(self.paths.get(path)): SwaggerEditor.validate_path_item_is_dict(path_item, path) - self.security_definitions = self._doc.get("securityDefinitions", Py27Dict()) - self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, Py27Dict()) - self.resource_policy = self._doc.get(self._X_APIGW_POLICY, Py27Dict()) - self.definitions = self._doc.get("definitions", Py27Dict()) - def get_conditional_contents(self, item): """ Returns the contents of the given item. @@ -165,6 +166,7 @@ def add_path(self, path, method=None): :param string path: Path name :param string method: HTTP method + :raises ValueError: If the value of `path` in Swagger is not a dictionary """ method = self._normalize_method_name(method) @@ -1284,32 +1286,6 @@ def is_valid(data): ) return False - def validate_definition_body(self, definition_body): - """ - Checks if definition_body is a valid Swagger document - - :param dict definition_body: Data to be validated - :raises InvalidDocumentException if definition_body is invalid - """ - - SwaggerEditor.validate_is_dict(definition_body, "DefinitionBody must be a dictionary.") - SwaggerEditor.validate_is_dict( - definition_body.get("paths"), "The 'paths' property of DefinitionBody must be present and be a dictionary." - ) - - has_swagger = definition_body.get("swagger") - has_openapi3 = definition_body.get("openapi") and SwaggerEditor.safe_compare_regex_with_string( - SwaggerEditor.get_openapi_version_3_regex(), definition_body["openapi"] - ) - if not (has_swagger) and not (has_openapi3): - raise InvalidDocumentException( - [ - InvalidTemplateException( - "DefinitionBody must have either: (1) a 'swagger' property or (2) an 'openapi' property with version 3.x or 3.x.x" - ) - ] - ) - @staticmethod def validate_is_dict(obj, exception_message): """ diff --git a/tests/model/api/test_api_generator.py b/tests/model/api/test_api_generator.py index c020085cf..391b3606f 100644 --- a/tests/model/api/test_api_generator.py +++ b/tests/model/api/test_api_generator.py @@ -18,7 +18,7 @@ def test_construct_usage_plan_with_invalid_usage_plan_type(self, invalid_usage_p Mock(), Mock(), Mock(), - {"paths": {}, "openapi": "3.0.1"}, + Mock(), Mock(), Mock(), Mock(), @@ -39,7 +39,7 @@ def test_construct_usage_plan_with_invalid_usage_plan_fields(self, AuthPropertie Mock(), Mock(), Mock(), - {"paths": {}, "openapi": "3.0.1"}, + Mock(), Mock(), Mock(), Mock(), diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index 3214b5c2d..884329219 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -18,7 +18,7 @@ class TestSwaggerEditor_init(TestCase): def test_must_raise_on_invalid_swagger(self): invalid_swagger = {"paths": {}} # Missing "Swagger" keyword - with self.assertRaises(InvalidDocumentException): + with self.assertRaises(ValueError): SwaggerEditor(invalid_swagger) def test_must_succeed_on_valid_swagger(self): @@ -32,13 +32,13 @@ def test_must_succeed_on_valid_swagger(self): def test_must_fail_on_invalid_openapi_version(self): invalid_swagger = {"openapi": "2.3.0", "paths": {"/foo": {}, "/bar": {}}} - with self.assertRaises(InvalidDocumentException): + with self.assertRaises(ValueError): SwaggerEditor(invalid_swagger) def test_must_fail_on_invalid_openapi_version_2(self): invalid_swagger = {"openapi": "3.1.1.1", "paths": {"/foo": {}, "/bar": {}}} - with self.assertRaises(InvalidDocumentException): + with self.assertRaises(ValueError): SwaggerEditor(invalid_swagger) def test_must_succeed_on_valid_openapi3(self): @@ -53,7 +53,7 @@ def test_must_succeed_on_valid_openapi3(self): def test_must_fail_with_bad_values_for_path(self, invalid_path_item): invalid_swagger = {"openapi": "3.1.1.1", "paths": {"/foo": {}, "/bad": invalid_path_item}} - with self.assertRaises(InvalidDocumentException): + with self.assertRaises(ValueError): SwaggerEditor(invalid_swagger) diff --git a/tests/translator/input/api_with_resource_refs.yaml b/tests/translator/input/api_with_resource_refs.yaml index 13a562909..e84845cbb 100644 --- a/tests/translator/input/api_with_resource_refs.yaml +++ b/tests/translator/input/api_with_resource_refs.yaml @@ -6,8 +6,8 @@ Resources: Properties: StageName: foo DefinitionBody: - paths: {} - openapi: "3.0.1" + "this": "is" + "a": "swagger" MyFunction: Type: "AWS::Serverless::Function" diff --git a/tests/translator/input/error_api_definition_body_invalid_openapi_version.yaml b/tests/translator/input/error_api_definition_body_invalid_openapi_version.yaml deleted file mode 100644 index c1f8ab2da..000000000 --- a/tests/translator/input/error_api_definition_body_invalid_openapi_version.yaml +++ /dev/null @@ -1,13 +0,0 @@ -Resources: - MyApi: - Type: AWS::Serverless::Api - Properties: - StageName: Prod - DefinitionBody: - info: - version: '1.0' - title: 'title' - paths: - "/some/path": {} - "/other": {} - openapi: '2.0' \ No newline at end of file diff --git a/tests/translator/input/error_api_definition_body_invalid_paths.yaml b/tests/translator/input/error_api_definition_body_invalid_paths.yaml deleted file mode 100644 index 40995b908..000000000 --- a/tests/translator/input/error_api_definition_body_invalid_paths.yaml +++ /dev/null @@ -1,11 +0,0 @@ -Resources: - MyApi: - Type: AWS::Serverless::Api - Properties: - StageName: Prod - DefinitionBody: - info: - version: '1.0' - title: 'title' - openapi: 3.0.1 - paths: 'invalid' \ No newline at end of file diff --git a/tests/translator/input/error_api_definition_body_missing_openapi_or_swagger.yaml b/tests/translator/input/error_api_definition_body_missing_openapi_or_swagger.yaml deleted file mode 100644 index 38ca085de..000000000 --- a/tests/translator/input/error_api_definition_body_missing_openapi_or_swagger.yaml +++ /dev/null @@ -1,12 +0,0 @@ -Resources: - MyApi: - Type: AWS::Serverless::Api - Properties: - StageName: Prod - DefinitionBody: - info: - version: '1.0' - title: 'title' - paths: - "/some/path": {} - "/other": {} \ No newline at end of file diff --git a/tests/translator/input/error_api_definition_body_missing_paths.yaml b/tests/translator/input/error_api_definition_body_missing_paths.yaml deleted file mode 100644 index 15de080b5..000000000 --- a/tests/translator/input/error_api_definition_body_missing_paths.yaml +++ /dev/null @@ -1,10 +0,0 @@ -Resources: - MyApi: - Type: AWS::Serverless::Api - Properties: - StageName: Prod - DefinitionBody: - info: - version: '1.0' - title: 'title' - openapi: 3.0.1 \ No newline at end of file diff --git a/tests/translator/input/error_api_invalid_auth.yaml b/tests/translator/input/error_api_invalid_auth.yaml index 92d8c25cc..60d2b3f9d 100644 --- a/tests/translator/input/error_api_invalid_auth.yaml +++ b/tests/translator/input/error_api_invalid_auth.yaml @@ -121,6 +121,14 @@ Resources: Auth: MyBad: Foo + AuthWithInvalidDefinitionBodyApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { invalid: true } + Auth: + DefaultAuthorizer: Foo + AuthWithMissingDefaultAuthorizerApi: Type: AWS::Serverless::Api Properties: diff --git a/tests/translator/input/error_api_invalid_definitionuri.yaml b/tests/translator/input/error_api_invalid_definitionuri.yaml index 90e9b09ad..371b3872a 100644 --- a/tests/translator/input/error_api_invalid_definitionuri.yaml +++ b/tests/translator/input/error_api_invalid_definitionuri.yaml @@ -18,5 +18,5 @@ Resources: StageName: Prod DefinitionUri: s3://foo/bar DefinitionBody: - paths: {} - openapi: "3.0.1" + a: b + c: d diff --git a/tests/translator/input/error_api_invalid_request_model.yaml b/tests/translator/input/error_api_invalid_request_model.yaml index f0679d456..45616dffa 100644 --- a/tests/translator/input/error_api_invalid_request_model.yaml +++ b/tests/translator/input/error_api_invalid_request_model.yaml @@ -68,6 +68,18 @@ Resources: username: type: string + ModelsWithInvalidDefinitionBodyApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + DefinitionBody: { invalid: true } + Models: + User: + type: object + properties: + username: + type: string + ModelIsNotString: Type: AWS::Serverless::Function Properties: diff --git a/tests/translator/input/explicit_api.yaml b/tests/translator/input/explicit_api.yaml index 3fb5534f2..a2057640c 100644 --- a/tests/translator/input/explicit_api.yaml +++ b/tests/translator/input/explicit_api.yaml @@ -45,5 +45,5 @@ Resources: StageName: Ref: MyStageName DefinitionBody: - paths: {} - swagger: "2.0" + "this": "is" + "a": "inline swagger" diff --git a/tests/translator/input/explicit_api_openapi_3.yaml b/tests/translator/input/explicit_api_openapi_3.yaml index f427521e7..a6f00e5e0 100644 --- a/tests/translator/input/explicit_api_openapi_3.yaml +++ b/tests/translator/input/explicit_api_openapi_3.yaml @@ -46,5 +46,5 @@ Resources: Ref: MyStageName OpenApiVersion: '3.0' DefinitionBody: - paths: {} - openapi: "3.0" + "this": "is" + "a": "inline swagger" diff --git a/tests/translator/output/api_with_resource_refs.json b/tests/translator/output/api_with_resource_refs.json index fc171a8b2..e32db402a 100644 --- a/tests/translator/output/api_with_resource_refs.json +++ b/tests/translator/output/api_with_resource_refs.json @@ -99,15 +99,15 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "openapi": "3.0.1", - "paths": {} + "this": "is", + "a": "swagger" } } }, - "MyApiDeploymentd7fcdf1086": { + "MyApiDeployment359f256a3b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { - "Description": "RestApi deployment id: d7fcdf1086262180dfe8b7ad5a04cab235490858", + "Description": "RestApi deployment id: 359f256a3b3ff2e1102e335a4d603f02df9b4988", "RestApiId": { "Ref": "MyApi" }, @@ -118,7 +118,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "MyApiDeploymentd7fcdf1086" + "Ref": "MyApiDeployment359f256a3b" }, "RestApiId": { "Ref": "MyApi" @@ -202,7 +202,7 @@ }, "ExplicitApiDeployment": { "Value": { - "Ref": "MyApiDeploymentd7fcdf1086" + "Ref": "MyApiDeployment359f256a3b" } }, "ExplicitApiStage": { diff --git a/tests/translator/output/aws-cn/api_with_resource_refs.json b/tests/translator/output/aws-cn/api_with_resource_refs.json index 2373bc45b..42444227a 100644 --- a/tests/translator/output/aws-cn/api_with_resource_refs.json +++ b/tests/translator/output/aws-cn/api_with_resource_refs.json @@ -99,8 +99,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "openapi": "3.0.1", - "paths": {} + "this": "is", + "a": "swagger" }, "Parameters": { "endpointConfigurationTypes": "REGIONAL" @@ -112,10 +112,10 @@ } } }, - "MyApiDeploymentd7fcdf1086": { + "MyApiDeployment359f256a3b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { - "Description": "RestApi deployment id: d7fcdf1086262180dfe8b7ad5a04cab235490858", + "Description": "RestApi deployment id: 359f256a3b3ff2e1102e335a4d603f02df9b4988", "RestApiId": { "Ref": "MyApi" }, @@ -126,7 +126,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "MyApiDeploymentd7fcdf1086" + "Ref": "MyApiDeployment359f256a3b" }, "RestApiId": { "Ref": "MyApi" @@ -218,7 +218,7 @@ }, "ExplicitApiDeployment": { "Value": { - "Ref": "MyApiDeploymentd7fcdf1086" + "Ref": "MyApiDeployment359f256a3b" } }, "ExplicitApiStage": { diff --git a/tests/translator/output/aws-cn/explicit_api.json b/tests/translator/output/aws-cn/explicit_api.json index 2bb41a25c..81fb57d35 100644 --- a/tests/translator/output/aws-cn/explicit_api.json +++ b/tests/translator/output/aws-cn/explicit_api.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment4fc299d023" + "Ref": "ApiWithInlineSwaggerDeployment09cda3d97b" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -93,8 +93,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "swagger": "2.0", - "paths": {} + "this": "is", + "a": "inline swagger" }, "EndpointConfiguration": { "Types": [ @@ -165,13 +165,13 @@ } } }, - "ApiWithInlineSwaggerDeployment4fc299d023": { + "ApiWithInlineSwaggerDeployment09cda3d97b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 4fc299d0231d133ea66a1aa4be7d5bcd64d5f900", + "Description": "RestApi deployment id: 09cda3d97b008bed7bd4ebb1b5304ed622492941", "StageName": "Stage" } } diff --git a/tests/translator/output/aws-cn/explicit_api_openapi_3.json b/tests/translator/output/aws-cn/explicit_api_openapi_3.json index 107dc6788..38e863d3a 100644 --- a/tests/translator/output/aws-cn/explicit_api_openapi_3.json +++ b/tests/translator/output/aws-cn/explicit_api_openapi_3.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment532ddc6618" + "Ref": "ApiWithInlineSwaggerDeployment74abcb3a5b" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -57,13 +57,13 @@ "StageName": "Stage" } }, - "ApiWithInlineSwaggerDeployment532ddc6618": { + "ApiWithInlineSwaggerDeployment74abcb3a5b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 532ddc6618dbe66a96a30ed7082f375de8933a02" + "Description": "RestApi deployment id: 74abcb3a5bbe7ad58dfc543740af3be156736130" } }, "GetHtmlFunctionRole": { @@ -102,8 +102,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "openapi": "3.0", - "paths": {} + "this": "is", + "a": "inline swagger" }, "EndpointConfiguration": { "Types": [ diff --git a/tests/translator/output/aws-us-gov/api_with_resource_refs.json b/tests/translator/output/aws-us-gov/api_with_resource_refs.json index 357b91be2..488db17e9 100644 --- a/tests/translator/output/aws-us-gov/api_with_resource_refs.json +++ b/tests/translator/output/aws-us-gov/api_with_resource_refs.json @@ -99,8 +99,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "openapi": "3.0.1", - "paths": {} + "this": "is", + "a": "swagger" }, "Parameters": { "endpointConfigurationTypes": "REGIONAL" @@ -112,10 +112,10 @@ } } }, - "MyApiDeploymentd7fcdf1086": { + "MyApiDeployment359f256a3b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { - "Description": "RestApi deployment id: d7fcdf1086262180dfe8b7ad5a04cab235490858", + "Description": "RestApi deployment id: 359f256a3b3ff2e1102e335a4d603f02df9b4988", "RestApiId": { "Ref": "MyApi" }, @@ -126,7 +126,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "MyApiDeploymentd7fcdf1086" + "Ref": "MyApiDeployment359f256a3b" }, "RestApiId": { "Ref": "MyApi" @@ -218,7 +218,7 @@ }, "ExplicitApiDeployment": { "Value": { - "Ref": "MyApiDeploymentd7fcdf1086" + "Ref": "MyApiDeployment359f256a3b" } }, "ExplicitApiStage": { diff --git a/tests/translator/output/aws-us-gov/explicit_api.json b/tests/translator/output/aws-us-gov/explicit_api.json index 0e367a86d..5c2d3ec05 100644 --- a/tests/translator/output/aws-us-gov/explicit_api.json +++ b/tests/translator/output/aws-us-gov/explicit_api.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment4fc299d023" + "Ref": "ApiWithInlineSwaggerDeployment09cda3d97b" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -93,8 +93,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "swagger": "2.0", - "paths": {} + "this": "is", + "a": "inline swagger" }, "EndpointConfiguration": { "Types": [ @@ -165,13 +165,13 @@ } } }, - "ApiWithInlineSwaggerDeployment4fc299d023": { + "ApiWithInlineSwaggerDeployment09cda3d97b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 4fc299d0231d133ea66a1aa4be7d5bcd64d5f900", + "Description": "RestApi deployment id: 09cda3d97b008bed7bd4ebb1b5304ed622492941", "StageName": "Stage" } } diff --git a/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json b/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json index 60f842de4..49592d0b2 100644 --- a/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json +++ b/tests/translator/output/aws-us-gov/explicit_api_openapi_3.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment532ddc6618" + "Ref": "ApiWithInlineSwaggerDeployment74abcb3a5b" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -57,13 +57,13 @@ "StageName": "Stage" } }, - "ApiWithInlineSwaggerDeployment532ddc6618": { + "ApiWithInlineSwaggerDeployment74abcb3a5b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 532ddc6618dbe66a96a30ed7082f375de8933a02" + "Description": "RestApi deployment id: 74abcb3a5bbe7ad58dfc543740af3be156736130" } }, "GetHtmlFunctionRole": { @@ -102,8 +102,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "openapi": "3.0", - "paths": {} + "this": "is", + "a": "inline swagger" }, "EndpointConfiguration": { "Types": [ diff --git a/tests/translator/output/error_api_definition_body_invalid_openapi_version.json b/tests/translator/output/error_api_definition_body_invalid_openapi_version.json deleted file mode 100644 index 72704bd99..000000000 --- a/tests/translator/output/error_api_definition_body_invalid_openapi_version.json +++ /dev/null @@ -1 +0,0 @@ -{"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. DefinitionBody must have either: (1) a 'swagger' property or (2) an 'openapi' property with version 3.x or 3.x.x"} \ No newline at end of file diff --git a/tests/translator/output/error_api_definition_body_invalid_paths.json b/tests/translator/output/error_api_definition_body_invalid_paths.json deleted file mode 100644 index e0180529f..000000000 --- a/tests/translator/output/error_api_definition_body_invalid_paths.json +++ /dev/null @@ -1 +0,0 @@ -{"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. The 'paths' property of DefinitionBody must be present and be a dictionary."} \ No newline at end of file diff --git a/tests/translator/output/error_api_definition_body_missing_openapi_or_swagger.json b/tests/translator/output/error_api_definition_body_missing_openapi_or_swagger.json deleted file mode 100644 index 72704bd99..000000000 --- a/tests/translator/output/error_api_definition_body_missing_openapi_or_swagger.json +++ /dev/null @@ -1 +0,0 @@ -{"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. DefinitionBody must have either: (1) a 'swagger' property or (2) an 'openapi' property with version 3.x or 3.x.x"} \ No newline at end of file diff --git a/tests/translator/output/error_api_definition_body_missing_paths.json b/tests/translator/output/error_api_definition_body_missing_paths.json deleted file mode 100644 index e0180529f..000000000 --- a/tests/translator/output/error_api_definition_body_missing_paths.json +++ /dev/null @@ -1 +0,0 @@ -{"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. The 'paths' property of DefinitionBody must be present and be a dictionary."} \ No newline at end of file diff --git a/tests/translator/output/error_api_invalid_auth.json b/tests/translator/output/error_api_invalid_auth.json index 06f59ee18..323721f3b 100644 --- a/tests/translator/output/error_api_invalid_auth.json +++ b/tests/translator/output/error_api_invalid_auth.json @@ -1,3 +1,3 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 15. Resource with id [AuthNotDictApi] is invalid. Type of property 'Auth' is invalid. Resource with id [AuthWithAdditionalPropertyApi] is invalid. Invalid value for 'Auth' property Resource with id [AuthWithDefinitionUriApi] is invalid. Auth works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [AuthWithMissingDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because 'NotThere' was not defined in 'Authorizers'. Resource with id [AuthorizerNotDict] is invalid. Authorizer MyCognitoAuthorizer must be a dictionary. Resource with id [AuthorizersNotDictApi] is invalid. Authorizers must be a dictionary. Resource with id [InvalidFunctionPayloadTypeApi] is invalid. MyLambdaAuthorizer Authorizer has invalid 'FunctionPayloadType': INVALID. Resource with id [MissingAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [UnspecifiedAuthorizer] on API method [get] for path [/] because it wasn't defined in the API's Authorizers. Resource with id [NoApiAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthorizersFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoDefaultAuthorizerWithNoneFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer on API method [get] for path [/] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [NoIdentityOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NoIdentitySourceOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NonStringDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because intrinsic functions are not supported for this field." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 16. Resource with id [AuthNotDictApi] is invalid. Type of property 'Auth' is invalid. Resource with id [AuthWithAdditionalPropertyApi] is invalid. Invalid value for 'Auth' property Resource with id [AuthWithDefinitionUriApi] is invalid. Auth works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [AuthWithInvalidDefinitionBodyApi] is invalid. Unable to add Auth configuration because 'DefinitionBody' does not contain a valid Swagger definition. Resource with id [AuthWithMissingDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because 'NotThere' was not defined in 'Authorizers'. Resource with id [AuthorizerNotDict] is invalid. Authorizer MyCognitoAuthorizer must be a dictionary. Resource with id [AuthorizersNotDictApi] is invalid. Authorizers must be a dictionary. Resource with id [InvalidFunctionPayloadTypeApi] is invalid. MyLambdaAuthorizer Authorizer has invalid 'FunctionPayloadType': INVALID. Resource with id [MissingAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [UnspecifiedAuthorizer] on API method [get] for path [/] because it wasn't defined in the API's Authorizers. Resource with id [NoApiAuthorizerFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoAuthorizersFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer [MyAuth] on API method [get] for path [/] because the related API does not define any Authorizers. Resource with id [NoDefaultAuthorizerWithNoneFn] is invalid. Event with id [GetRoot] is invalid. Unable to set Authorizer on API method [get] for path [/] because 'NONE' is only a valid value when a DefaultAuthorizer on the API is specified. Resource with id [NoIdentityOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NoIdentitySourceOnRequestAuthorizer] is invalid. MyLambdaRequestAuthorizer Authorizer must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. Resource with id [NonStringDefaultAuthorizerApi] is invalid. Unable to set DefaultAuthorizer because intrinsic functions are not supported for this field." } diff --git a/tests/translator/output/error_api_invalid_request_model.json b/tests/translator/output/error_api_invalid_request_model.json index 7c3422cac..e015f1b5e 100644 --- a/tests/translator/output/error_api_invalid_request_model.json +++ b/tests/translator/output/error_api_invalid_request_model.json @@ -1,3 +1,3 @@ { - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 5. Resource with id [MissingModelFunction] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [UnspecifiedModel] on API method [get] for path [/] because it wasn't defined in the API's Models. Resource with id [ModelIsNotString] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [['NotString']] on API method [get] for path [/] because the related API does not contain valid Models. Resource with id [ModelsNotDictApi] is invalid. Invalid value for 'Models' property Resource with id [ModelsWithDefinitionUrlApi] is invalid. Models works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [NoModelFunction] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [User] on API method [get] for path [/] because the related API does not define any Models." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 6. Resource with id [MissingModelFunction] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [UnspecifiedModel] on API method [get] for path [/] because it wasn't defined in the API's Models. Resource with id [ModelIsNotString] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [['NotString']] on API method [get] for path [/] because the related API does not contain valid Models. Resource with id [ModelsNotDictApi] is invalid. Invalid value for 'Models' property Resource with id [ModelsWithDefinitionUrlApi] is invalid. Models works only with inline Swagger specified in 'DefinitionBody' property. Resource with id [ModelsWithInvalidDefinitionBodyApi] is invalid. Unable to add Models definitions because 'DefinitionBody' does not contain a valid Swagger definition. Resource with id [NoModelFunction] is invalid. Event with id [GetHtml] is invalid. Unable to set RequestModel [User] on API method [get] for path [/] because the related API does not define any Models." } diff --git a/tests/translator/output/explicit_api.json b/tests/translator/output/explicit_api.json index 6b64fca3c..cfc5f2e0f 100644 --- a/tests/translator/output/explicit_api.json +++ b/tests/translator/output/explicit_api.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment4fc299d023" + "Ref": "ApiWithInlineSwaggerDeployment09cda3d97b" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -93,8 +93,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "swagger": "2.0", - "paths": {} + "this": "is", + "a": "inline swagger" } } }, @@ -149,13 +149,13 @@ } } }, - "ApiWithInlineSwaggerDeployment4fc299d023": { + "ApiWithInlineSwaggerDeployment09cda3d97b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 4fc299d0231d133ea66a1aa4be7d5bcd64d5f900", + "Description": "RestApi deployment id: 09cda3d97b008bed7bd4ebb1b5304ed622492941", "StageName": "Stage" } } diff --git a/tests/translator/output/explicit_api_openapi_3.json b/tests/translator/output/explicit_api_openapi_3.json index cdec27f9f..4397a036c 100644 --- a/tests/translator/output/explicit_api_openapi_3.json +++ b/tests/translator/output/explicit_api_openapi_3.json @@ -37,7 +37,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ApiWithInlineSwaggerDeployment532ddc6618" + "Ref": "ApiWithInlineSwaggerDeployment74abcb3a5b" }, "RestApiId": { "Ref": "ApiWithInlineSwagger" @@ -57,13 +57,13 @@ "StageName": "Stage" } }, - "ApiWithInlineSwaggerDeployment532ddc6618": { + "ApiWithInlineSwaggerDeployment74abcb3a5b": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ApiWithInlineSwagger" }, - "Description": "RestApi deployment id: 532ddc6618dbe66a96a30ed7082f375de8933a02" + "Description": "RestApi deployment id: 74abcb3a5bbe7ad58dfc543740af3be156736130" } }, "GetHtmlFunctionRole": { @@ -102,8 +102,8 @@ "Type": "AWS::ApiGateway::RestApi", "Properties": { "Body": { - "openapi": "3.0", - "paths": {} + "this": "is", + "a": "inline swagger" } } }, diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index f94534464..936f31c95 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -761,8 +761,7 @@ def test_swagger_body_sha_gets_recomputed(): "StageName": "Prod", "DefinitionBody": { # Some body property will do - "paths": {}, - "openapi": "3.0.1", + "a": "b" }, }, } From 594c54ff40c537dd281d38346eea7d786304fe81 Mon Sep 17 00:00:00 2001 From: Wing Fung Lau <4760060+hawflau@users.noreply.github.com> Date: Mon, 14 Mar 2022 17:00:54 -0700 Subject: [PATCH 59/59] chore: Version bump to 1.43.0 (#2347) --- samtranslator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index 5d0a428bc..eb012bb42 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.42.0" +__version__ = "1.43.0"