diff --git a/cloudformation/serverless/api_cors.go b/cloudformation/serverless/api_cors.go new file mode 100644 index 0000000000..194610899e --- /dev/null +++ b/cloudformation/serverless/api_cors.go @@ -0,0 +1,64 @@ +package serverless + +import ( + "encoding/json" + "sort" + + "github.com/awslabs/goformation/v4/cloudformation/utils" +) + +// Api_Cors is a helper struct that can hold either a String or CorsConfiguration value +type Api_Cors struct { + String *string + + CorsConfiguration *Api_CorsConfiguration +} + +func (r Api_Cors) value() interface{} { + ret := []interface{}{} + + if r.String != nil { + ret = append(ret, r.String) + } + + if r.CorsConfiguration != nil { + ret = append(ret, *r.CorsConfiguration) + } + + sort.Sort(utils.ByJSONLength(ret)) // Heuristic to select best attribute + if len(ret) > 0 { + return ret[0] + } + + return nil +} + +func (r Api_Cors) MarshalJSON() ([]byte, error) { + return json.Marshal(r.value()) +} + +// Hook into the marshaller +func (r *Api_Cors) UnmarshalJSON(b []byte) error { + + // Unmarshal into interface{} to check it's type + var typecheck interface{} + if err := json.Unmarshal(b, &typecheck); err != nil { + return err + } + + switch val := typecheck.(type) { + + case string: + r.String = &val + + case map[string]interface{}: + val = val // This ensures val is used to stop an error + + json.Unmarshal(b, &r.CorsConfiguration) + + case []interface{}: + + } + + return nil +} diff --git a/cloudformation/serverless/aws-serverless-api.go b/cloudformation/serverless/aws-serverless-api.go index 6e2bd885a7..d974e895b1 100644 --- a/cloudformation/serverless/aws-serverless-api.go +++ b/cloudformation/serverless/aws-serverless-api.go @@ -40,7 +40,7 @@ type Api struct { // Cors AWS CloudFormation Property // Required: false // See: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessapi - Cors string `json:"Cors,omitempty"` + Cors *Api_Cors `json:"Cors,omitempty"` // DefinitionBody AWS CloudFormation Property // Required: false diff --git a/cloudformation/serverless/aws-serverless-api_corsconfiguration.go b/cloudformation/serverless/aws-serverless-api_corsconfiguration.go new file mode 100644 index 0000000000..bfdb808eef --- /dev/null +++ b/cloudformation/serverless/aws-serverless-api_corsconfiguration.go @@ -0,0 +1,49 @@ +package serverless + +import ( + "github.com/awslabs/goformation/v4/cloudformation/policies" +) + +// Api_CorsConfiguration AWS CloudFormation Resource (AWS::Serverless::Api.CorsConfiguration) +// See: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration +type Api_CorsConfiguration struct { + + // AllowCredentials AWS CloudFormation Property + // Required: false + // See: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration + AllowCredentials bool `json:"AllowCredentials,omitempty"` + + // AllowHeaders AWS CloudFormation Property + // Required: false + // See: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration + AllowHeaders string `json:"AllowHeaders,omitempty"` + + // AllowMethods AWS CloudFormation Property + // Required: false + // See: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration + AllowMethods string `json:"AllowMethods,omitempty"` + + // AllowOrigin AWS CloudFormation Property + // Required: true + // See: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration + AllowOrigin string `json:"AllowOrigin,omitempty"` + + // MaxAge AWS CloudFormation Property + // Required: false + // See: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration + MaxAge string `json:"MaxAge,omitempty"` + + // AWSCloudFormationDeletionPolicy represents a CloudFormation DeletionPolicy + AWSCloudFormationDeletionPolicy policies.DeletionPolicy `json:"-"` + + // AWSCloudFormationDependsOn stores the logical ID of the resources to be created before this resource + AWSCloudFormationDependsOn []string `json:"-"` + + // AWSCloudFormationMetadata stores structured data associated with this resource + AWSCloudFormationMetadata map[string]interface{} `json:"-"` +} + +// AWSCloudFormationType returns the AWS CloudFormation resource type +func (r *Api_CorsConfiguration) AWSCloudFormationType() string { + return "AWS::Serverless::Api.CorsConfiguration" +} diff --git a/generate/sam-2016-10-31.json b/generate/sam-2016-10-31.json index 91b807ffa5..dd04c2a2b4 100644 --- a/generate/sam-2016-10-31.json +++ b/generate/sam-2016-10-31.json @@ -227,7 +227,12 @@ "Cors": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessapi", "Required": false, - "PrimitiveType": "String", + "PrimitiveTypes": [ + "String" + ], + "Types": [ + "CorsConfiguration" + ], "UpdateType": "Immutable" }, "Auth": { @@ -256,8 +261,12 @@ "Location": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessapplication", "Required": true, - "PrimitiveTypes": [ "String" ], - "Types": [ "ApplicationLocation" ], + "PrimitiveTypes": [ + "String" + ], + "Types": [ + "ApplicationLocation" + ], "UpdateType": "Immutable" }, "Parameters": { @@ -909,7 +918,6 @@ } } }, - "AWS::Serverless::Function.DomainSAMPT": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/docs/policy_templates.rst", "Properties": { @@ -1121,6 +1129,41 @@ } } }, + "AWS::Serverless::Api.CorsConfiguration": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration", + "Properties": { + "AllowMethods": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration", + "Required": false, + "PrimitiveType": "String", + "UpdateType": "Immutable" + }, + "AllowHeaders": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration", + "Required": false, + "PrimitiveType": "String", + "UpdateType": "Immutable" + }, + "AllowOrigin": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration", + "Required": true, + "PrimitiveType": "String", + "UpdateType": "Immutable" + }, + "MaxAge": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration", + "Required": false, + "PrimitiveType": "String", + "UpdateType": "Immutable" + }, + "AllowCredentials": { + "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#cors-configuration", + "Required": false, + "PrimitiveType": "Boolean", + "UpdateType": "Immutable" + } + } + }, "AWS::Serverless::SimpleTable.PrimaryKey": { "Documentation": "https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#primary-key-object", "Properties": { @@ -1167,4 +1210,4 @@ } } } -} +} \ No newline at end of file diff --git a/goformation_test.go b/goformation_test.go index 6c4fb12416..52ffb415a9 100644 --- a/goformation_test.go +++ b/goformation_test.go @@ -297,6 +297,35 @@ var _ = Describe("Goformation", func() { }) + Context("with a Serverless template containing different CORS configuration formats", func() { + + template, err := goformation.Open("test/yaml/aws-serverless-api-string-or-cors-configuration.yaml") + It("should successfully parse the template", func() { + Expect(err).To(BeNil()) + Expect(template).ShouldNot(BeNil()) + }) + + apis := template.GetAllServerlessApiResources() + + It("should have exactly two APIs", func() { + Expect(apis).To(HaveLen(2)) + Expect(apis).To(HaveKey("RestApiWithCorsConfiguration")) + Expect(apis).To(HaveKey("RestApiWithCorsString")) + }) + + api1 := apis["RestApiWithCorsConfiguration"] + It("should parse a Cors configuration object", func() { + Expect(api1.Cors.CorsConfiguration.AllowHeaders).To(Equal("'Authorization,authorization'")) + Expect(api1.Cors.CorsConfiguration.AllowOrigin).To(Equal("'*'")) + }) + + api2 := apis["RestApiWithCorsString"] + It("should parse a Cors string", func() { + Expect(api2.Cors.String).To(PointTo(Equal("'www.example.com'"))) + }) + + }) + Context("with a Serverless template containing different CodeUri formats", func() { template, err := goformation.Open("test/yaml/aws-serverless-function-string-or-s3-location.yaml") diff --git a/schema/sam.go b/schema/sam.go index bf61c2138d..b59280c7c8 100644 --- a/schema/sam.go +++ b/schema/sam.go @@ -52099,7 +52099,16 @@ var SamSchema = `{ "type": "string" }, "Cors": { - "type": "string" + "anyOf": [ + { + "type": [ + "string" + ] + }, + { + "$ref": "#/definitions/AWS::Serverless::Api.CorsConfiguration" + } + ] }, "DefinitionBody": { "type": "object" @@ -52186,6 +52195,30 @@ var SamSchema = `{ }, "type": "object" }, + "AWS::Serverless::Api.CorsConfiguration": { + "additionalProperties": false, + "properties": { + "AllowCredentials": { + "type": "boolean" + }, + "AllowHeaders": { + "type": "string" + }, + "AllowMethods": { + "type": "string" + }, + "AllowOrigin": { + "type": "string" + }, + "MaxAge": { + "type": "string" + } + }, + "required": [ + "AllowOrigin" + ], + "type": "object" + }, "AWS::Serverless::Api.S3Location": { "additionalProperties": false, "properties": { diff --git a/schema/sam.schema.json b/schema/sam.schema.json index ad100df5e0..4ca3e8b665 100644 --- a/schema/sam.schema.json +++ b/schema/sam.schema.json @@ -52096,7 +52096,16 @@ "type": "string" }, "Cors": { - "type": "string" + "anyOf": [ + { + "type": [ + "string" + ] + }, + { + "$ref": "#/definitions/AWS::Serverless::Api.CorsConfiguration" + } + ] }, "DefinitionBody": { "type": "object" @@ -52183,6 +52192,30 @@ }, "type": "object" }, + "AWS::Serverless::Api.CorsConfiguration": { + "additionalProperties": false, + "properties": { + "AllowCredentials": { + "type": "boolean" + }, + "AllowHeaders": { + "type": "string" + }, + "AllowMethods": { + "type": "string" + }, + "AllowOrigin": { + "type": "string" + }, + "MaxAge": { + "type": "string" + } + }, + "required": [ + "AllowOrigin" + ], + "type": "object" + }, "AWS::Serverless::Api.S3Location": { "additionalProperties": false, "properties": { diff --git a/test/yaml/aws-serverless-api-string-or-cors-configuration.yaml b/test/yaml/aws-serverless-api-string-or-cors-configuration.yaml new file mode 100644 index 0000000000..4ac4c4b7a6 --- /dev/null +++ b/test/yaml/aws-serverless-api-string-or-cors-configuration.yaml @@ -0,0 +1,38 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM template for testing AWS::Serverless::Api.Cors with both string and Cors Configuration object +Resources: + + RestApiWithCorsConfiguration: + Type: AWS::Serverless::Api + Properties: + Name: !Sub "${AWS::StackName}-rest-api" + StageName: Prod + TracingEnabled: false + EndpointConfiguration: EDGE + MethodSettings: + - DataTraceEnabled: false + HttpMethod: "*" + LoggingLevel: ERROR + MetricsEnabled: true + ResourcePath: /* + Cors: + AllowHeaders: "'Authorization,authorization'" + AllowOrigin: "'*'" + + RestApiWithCorsString: + Type: AWS::Serverless::Api + Properties: + Name: !Sub "${AWS::StackName}-rest-api" + StageName: Prod + TracingEnabled: false + EndpointConfiguration: EDGE + MethodSettings: + - DataTraceEnabled: false + HttpMethod: "*" + LoggingLevel: ERROR + MetricsEnabled: true + ResourcePath: /* + Cors: "'www.example.com'" + + \ No newline at end of file