diff --git a/bin/sam-translate.py b/bin/sam-translate.py index b5ca1d9441..5c67b0552b 100755 --- a/bin/sam-translate.py +++ b/bin/sam-translate.py @@ -21,6 +21,8 @@ from samtranslator.public.translator import ManagedPolicyLoader from samtranslator.translator.transform import transform from samtranslator.yaml_helper import yaml_parse +from samtranslator.model.exceptions import InvalidDocumentException + cli_options = docopt(__doc__) iam_client = boto3.client('iam') @@ -42,15 +44,21 @@ def main(): with open(input_file_path, 'r') as f: sam_template = yaml_parse(f) - cloud_formation_template = transform( - sam_template, {}, ManagedPolicyLoader(iam_client)) - cloud_formation_template_prettified = json.dumps( - cloud_formation_template, indent=2) - - with open(output_file_path, 'w') as f: - f.write(cloud_formation_template_prettified) - - print('Wrote transformed CloudFormation template to: ' + output_file_path) + try: + cloud_formation_template = transform( + sam_template, {}, ManagedPolicyLoader(iam_client)) + cloud_formation_template_prettified = json.dumps( + cloud_formation_template, indent=2) + + with open(output_file_path, 'w') as f: + f.write(cloud_formation_template_prettified) + + print('Wrote transformed CloudFormation template to: ' + output_file_path) + except InvalidDocumentException as e: + errorMessage = reduce(lambda message, error: message + ' ' + error.message, e.causes, e.message) + print(errorMessage) + errors = map(lambda cause: {'errorMessage': cause.message}, e.causes) + print(errors) if __name__ == '__main__': diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index cb2a195ebe..99cc72d47d 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -72,9 +72,6 @@ def _construct_rest_api(self): rest_api.BodyS3Location = self._construct_body_s3_dict() elif self.definition_body: rest_api.Body = self.definition_body - else: - raise InvalidResourceException(self.logical_id, - "Either 'DefinitionUri' or 'DefinitionBody' property is required") if self.name: rest_api.Name = self.name diff --git a/samtranslator/plugins/api/default_definition_body_plugin.py b/samtranslator/plugins/api/default_definition_body_plugin.py new file mode 100644 index 0000000000..3264ff7f06 --- /dev/null +++ b/samtranslator/plugins/api/default_definition_body_plugin.py @@ -0,0 +1,37 @@ +from samtranslator.plugins import BasePlugin +from samtranslator.swagger.swagger import SwaggerEditor +from samtranslator.public.sdk.resource import SamResourceType +from samtranslator.public.sdk.template import SamTemplate + + +class DefaultDefinitionBodyPlugin(BasePlugin): + """ + If the user does not provide a DefinitionBody or DefinitionUri + on an AWS::Serverless::Api resource, the Swagger constructed by + SAM is used. It accomplishes this by simply setting DefinitionBody + to a minimum Swagger definition and sets `__MANAGE_SWAGGER: true`. + """ + + def __init__(self): + """ + Initialize the plugin. + """ + + super(DefaultDefinitionBodyPlugin, self).__init__(DefaultDefinitionBodyPlugin.__name__) + + def on_before_transform_template(self, template_dict): + """ + Hook method that gets called before the SAM template is processed. + The template has passed the validation and is guaranteed to contain a non-empty "Resources" section. + + :param dict template_dict: Dictionary of the SAM template + :return: Nothing + """ + template = SamTemplate(template_dict) + + for logicalId, api in template.iterate(SamResourceType.Api.value): + if api.properties.get('DefinitionBody') or api.properties.get('DefinitionUri'): + continue + + api.properties['DefinitionBody'] = SwaggerEditor.gen_skeleton() + api.properties['__MANAGE_SWAGGER'] = True diff --git a/samtranslator/translator/translator.py b/samtranslator/translator/translator.py index 57f6c187f4..11adf850ed 100644 --- a/samtranslator/translator/translator.py +++ b/samtranslator/translator/translator.py @@ -7,6 +7,7 @@ InvalidEventException from samtranslator.intrinsics.resolver import IntrinsicsResolver from samtranslator.intrinsics.resource_refs import SupportedResourceReferences +from samtranslator.plugins.api.default_definition_body_plugin import DefaultDefinitionBodyPlugin from samtranslator.plugins import SamPlugins from samtranslator.plugins.globals.globals_plugin import GlobalsPlugin from samtranslator.plugins.policies.policy_templates_plugin import PolicyTemplatesForFunctionPlugin @@ -198,6 +199,7 @@ def prepare_plugins(plugins): """ required_plugins = [ + DefaultDefinitionBodyPlugin(), make_implicit_api_plugin(), GlobalsPlugin(), make_policy_template_for_function_plugin(), diff --git a/tests/plugins/api/test_default_definition_body_plugin.py b/tests/plugins/api/test_default_definition_body_plugin.py new file mode 100644 index 0000000000..60758ba0ce --- /dev/null +++ b/tests/plugins/api/test_default_definition_body_plugin.py @@ -0,0 +1,51 @@ +from mock import Mock, patch +from unittest import TestCase + +from samtranslator.plugins.api.default_definition_body_plugin import DefaultDefinitionBodyPlugin +from samtranslator.public.plugins import BasePlugin + +IMPLICIT_API_LOGICAL_ID = "ServerlessRestApi" + + +class TestDefaultDefinitionBodyPlugin_init(TestCase): + + def setUp(self): + self.plugin = DefaultDefinitionBodyPlugin() + + def test_plugin_must_setup_correct_name(self): + # Name is the class name + expected_name = "DefaultDefinitionBodyPlugin" + + self.assertEquals(self.plugin.name, expected_name) + + def test_plugin_must_be_instance_of_base_plugin_class(self): + self.assertTrue(isinstance(self.plugin, BasePlugin)) + + +class TestDefaultDefinitionBodyPlugin_on_before_transform_template(TestCase): + + def setUp(self): + self.plugin = DefaultDefinitionBodyPlugin() + + @patch("samtranslator.plugins.api.default_definition_body_plugin.SamTemplate") + def test_must_process_functions(self, SamTemplateMock): + + template_dict = {"a": "b"} + api_resources = [("id1", ApiResource()), ("id2", ApiResource()), ("id3", ApiResource())] + + sam_template = Mock() + SamTemplateMock.return_value = sam_template + sam_template.iterate = Mock() + sam_template.iterate.return_value = api_resources + + self.plugin.on_before_transform_template(template_dict) + + SamTemplateMock.assert_called_with(template_dict) + + # Make sure this is called only for Apis + sam_template.iterate.assert_called_with("AWS::Serverless::Api") + + +class ApiResource(object): + def __init__(self): + self.properties = {} diff --git a/tests/translator/input/api_with_cors_no_definitionbody.yaml b/tests/translator/input/api_with_cors_no_definitionbody.yaml new file mode 100644 index 0000000000..465422530f --- /dev/null +++ b/tests/translator/input/api_with_cors_no_definitionbody.yaml @@ -0,0 +1,34 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Globals: + Api: + # If we skip AllowMethods, then SAM will auto generate a list of methods scoped to each path + Cors: "origins" + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs4.3 + Events: + GetHtml: + Type: Api + Properties: + RestApiId: !Ref ExplicitApi + Path: / + Method: get + + PostHtml: + Type: Api + Properties: + RestApiId: !Ref ExplicitApi + Path: / + Method: post + + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod \ No newline at end of file diff --git a/tests/translator/output/api_with_cors_no_definitionbody.json b/tests/translator/output/api_with_cors_no_definitionbody.json new file mode 100644 index 0000000000..d4c2976cec --- /dev/null +++ b/tests/translator/output/api_with_cors_no_definitionbody.json @@ -0,0 +1,232 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "ImplicitApiFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "ImplicitApiFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [{ + "Value": "SAM", + "Key": "lambda:createdBy" + }] + } + }, + "ImplicitApiFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + }] + } + } + }, + "ImplicitApiFunctionPostHtmlPermissionTest": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionGetHtmlPermissionTest": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionPostHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/", + { + "__Stage__": "Prod", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "Prod", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + }, + "options": { + "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": "origins", + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS,POST'" + } + } + } + }, + "consumes": [ + "application/json" + ], + "summary": "CORS support", + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ] + }, + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + } + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeployment4be3cdc28b" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeployment4be3cdc28b": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ExplicitApi" + }, + "Description": "RestApi deployment id: 4be3cdc28b991a26bed9da1180e59e9cc5467355", + "StageName": "Stage" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_with_cors_no_definitionbody.json b/tests/translator/output/aws-cn/api_with_cors_no_definitionbody.json new file mode 100644 index 0000000000..ff72170ebd --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_cors_no_definitionbody.json @@ -0,0 +1,240 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "ImplicitApiFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "ImplicitApiFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [{ + "Value": "SAM", + "Key": "lambda:createdBy" + }] + } + }, + "ImplicitApiFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + }] + } + } + }, + "ImplicitApiFunctionPostHtmlPermissionTest": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionGetHtmlPermissionTest": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionPostHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/", + { + "__Stage__": "Prod", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "Prod", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + }, + "options": { + "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": "origins", + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS,POST'" + } + } + } + }, + "consumes": [ + "application/json" + ], + "summary": "CORS support", + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ] + }, + "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/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeploymentb4ba0b8ae8" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeploymentb4ba0b8ae8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ExplicitApi" + }, + "Description": "RestApi deployment id: b4ba0b8ae8163e6dd52040648ae4ebda76cc5d99", + "StageName": "Stage" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_with_cors_no_definitionbody.json b/tests/translator/output/aws-us-gov/api_with_cors_no_definitionbody.json new file mode 100644 index 0000000000..070ed12f59 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_cors_no_definitionbody.json @@ -0,0 +1,240 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": { + "ImplicitApiFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "member_portal.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "ImplicitApiFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs4.3", + "Tags": [{ + "Value": "SAM", + "Key": "lambda:createdBy" + }] + } + }, + "ImplicitApiFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + }] + } + } + }, + "ImplicitApiFunctionPostHtmlPermissionTest": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionGetHtmlPermissionTest": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionPostHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/", + { + "__Stage__": "Prod", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ImplicitApiFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:invokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "ImplicitApiFunction" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/", + { + "__Stage__": "Prod", + "__ApiId__": { + "Ref": "ExplicitApi" + } + } + ] + } + } + }, + "ExplicitApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/": { + "post": { + "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/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + }, + "options": { + "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": "origins", + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS,POST'" + } + } + } + }, + "consumes": [ + "application/json" + ], + "summary": "CORS support", + "responses": { + "200": { + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + } + }, + "description": "Default response for CORS method" + } + }, + "produces": [ + "application/json" + ] + }, + "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/${ImplicitApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + }, + "swagger": "2.0" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "ExplicitApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "ExplicitApiDeploymentc934a493f3" + }, + "RestApiId": { + "Ref": "ExplicitApi" + }, + "StageName": "Prod" + } + }, + "ExplicitApiDeploymentc934a493f3": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "ExplicitApi" + }, + "Description": "RestApi deployment id: c934a493f3473efce5bb2acbf1d54e9c2ce0fef3", + "StageName": "Stage" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/error_api_invalid_definitionuri.json b/tests/translator/output/error_api_invalid_definitionuri.json index 8c1b1fafd6..d7760185e0 100644 --- a/tests/translator/output/error_api_invalid_definitionuri.json +++ b/tests/translator/output/error_api_invalid_definitionuri.json @@ -2,13 +2,10 @@ "errors": [ { "errorMessage": "Resource with id [Api] is invalid. 'DefinitionUri' is not a valid S3 Uri of the form \"s3://bucket/key\" with optional versionId query parameter." - }, - { - "errorMessage": "Resource with id [ApiNoBody] is invalid. Either 'DefinitionUri' or 'DefinitionBody' property is required" - }, + }, { "errorMessage": "Resource with id [ApiWithBodyAndDefinitionUri] is invalid. Specify either 'DefinitionUri' or 'DefinitionBody' property and not both" } ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 3. Resource with id [Api] is invalid. 'DefinitionUri' is not a valid S3 Uri of the form \"s3://bucket/key\" with optional versionId query parameter. Resource with id [ApiNoBody] is invalid. Either 'DefinitionUri' or 'DefinitionBody' property is required Resource with id [ApiWithBodyAndDefinitionUri] is invalid. Specify either 'DefinitionUri' or 'DefinitionBody' property and not both" + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 2. Resource with id [Api] is invalid. 'DefinitionUri' is not a valid S3 Uri of the form \"s3://bucket/key\" with optional versionId query parameter. Resource with id [ApiWithBodyAndDefinitionUri] is invalid. Specify either 'DefinitionUri' or 'DefinitionBody' property and not both" } \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 19d9b24156..ca56197412 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -111,6 +111,7 @@ class TestTranslatorEndToEnd(TestCase): 'api_with_cors_and_only_headers', 'api_with_cors_and_only_origins', 'api_with_cors_and_only_maxage', + 'api_with_cors_no_definitionbody', 'api_cache', 's3', 's3_create_remove', @@ -679,7 +680,7 @@ def test_prepare_plugins_must_add_required_plugins(self, make_policy_template_fo make_policy_template_for_function_plugin_mock.return_value = plugin_instance sam_plugins = prepare_plugins([]) - self.assertEquals(3, len(sam_plugins)) + self.assertEquals(4, len(sam_plugins)) @patch("samtranslator.translator.translator.make_policy_template_for_function_plugin") def test_prepare_plugins_must_merge_input_plugins(self, make_policy_template_for_function_plugin_mock): @@ -689,12 +690,12 @@ def test_prepare_plugins_must_merge_input_plugins(self, make_policy_template_for custom_plugin = BasePlugin("someplugin") sam_plugins = prepare_plugins([custom_plugin]) - self.assertEquals(4, len(sam_plugins)) + self.assertEquals(5, len(sam_plugins)) def test_prepare_plugins_must_handle_empty_input(self): sam_plugins = prepare_plugins(None) - self.assertEquals(3, len(sam_plugins)) # one required plugin + self.assertEquals(4, len(sam_plugins)) @patch("samtranslator.translator.translator.PolicyTemplatesProcessor") @patch("samtranslator.translator.translator.PolicyTemplatesForFunctionPlugin")