diff --git a/cloudformation/policies_test.go b/cloudformation/policies_test.go index 78d749342b..6a05caebff 100644 --- a/cloudformation/policies_test.go +++ b/cloudformation/policies_test.go @@ -1,7 +1,7 @@ package cloudformation_test import ( - "github.com/sanathkr/yaml" + "gopkg.in/yaml.v3" "github.com/awslabs/goformation/v7/cloudformation" "github.com/awslabs/goformation/v7/cloudformation/autoscaling" @@ -47,9 +47,9 @@ var _ = Describe("Goformation", func() { }, Expected: map[string]interface{}{ "AutoScalingRollingUpdate": map[string]interface{}{ - "MaxBatchSize": float64(10), - "MinInstancesInService": float64(11), - "MinSuccessfulInstancesPercent": float64(12), + "MaxBatchSize": 10, + "MinInstancesInService": 11, + "MinSuccessfulInstancesPercent": 12, "PauseTime": "test-pause-time", "SuspendProcesses": []interface{}{"test-suspend1", "test-suspend2"}, "WaitOnResourceSignals": true, @@ -146,10 +146,10 @@ var _ = Describe("Goformation", func() { }, Expected: map[string]interface{}{ "AutoScalingCreationPolicy": map[string]interface{}{ - "MinSuccessfulInstancesPercent": float64(10), + "MinSuccessfulInstancesPercent": 10, }, "ResourceSignal": map[string]interface{}{ - "Count": float64(11), + "Count": 11, "Timeout": "test-timeout", }, }, diff --git a/cloudformation/template.go b/cloudformation/template.go index b47cb242cb..08e2088578 100644 --- a/cloudformation/template.go +++ b/cloudformation/template.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/awslabs/goformation/v7/intrinsics" - "github.com/sanathkr/yaml" + "gopkg.in/yaml.v3" ) // Template represents an AWS CloudFormation template @@ -231,12 +231,22 @@ func (t *Template) JSON() ([]byte, error) { // YAML converts an AWS CloudFormation template object to YAML func (t *Template) YAML() ([]byte, error) { - j, err := t.JSON() if err != nil { return nil, err } - return yaml.JSONToYAML(j) + var jsonObj interface{} + // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the + // Go JSON library doesn't try to pick the right number type (int, float, + // etc.) when unmarshalling to interface{}, it just picks float64 + // universally. go-yaml does go through the effort of picking the right + // number type, so we can preserve number type throughout this process. + err = yaml.Unmarshal(j, &jsonObj) + if err != nil { + return nil, err + } + // Marshal this object into YAML + return yaml.Marshal(jsonObj) } diff --git a/go.mod b/go.mod index 194dda2830..72cbf6e58a 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,8 @@ module github.com/awslabs/goformation/v7 require ( github.com/onsi/ginkgo/v2 v2.3.1 github.com/onsi/gomega v1.22.1 - github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b - github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -16,7 +15,6 @@ require ( golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) go 1.17 diff --git a/goformation_test.go b/goformation_test.go index e83d145c3a..d8366a31ab 100644 --- a/goformation_test.go +++ b/goformation_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/sanathkr/yaml" + "gopkg.in/yaml.v3" "github.com/awslabs/goformation/v7" "github.com/awslabs/goformation/v7/cloudformation" @@ -98,19 +98,19 @@ func Example_to_yaml() { } // Output: - // AWSTemplateFormatVersion: 2010-09-09 + // AWSTemplateFormatVersion: "2010-09-09" // Resources: - // MyTopic: - // Properties: - // TopicName: my-topic-1575143970 - // Type: AWS::SNS::Topic - // MyTopicSubscription: - // Properties: - // Endpoint: some.email@example.com - // Protocol: email - // TopicArn: - // Ref: MyTopic - // Type: AWS::SNS::Subscription + // MyTopic: + // Properties: + // TopicName: my-topic-1575143970 + // Type: AWS::SNS::Topic + // MyTopicSubscription: + // Properties: + // Endpoint: some.email@example.com + // Protocol: email + // TopicArn: + // Ref: MyTopic + // Type: AWS::SNS::Subscription } diff --git a/intrinsics/intrinsics.go b/intrinsics/intrinsics.go index 449937e239..750758b963 100644 --- a/intrinsics/intrinsics.go +++ b/intrinsics/intrinsics.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" - yamlwrapper "github.com/sanathkr/yaml" + "gopkg.in/yaml.v3" ) // IntrinsicHandler is a function that applies an intrinsic function and returns @@ -61,13 +61,21 @@ func ProcessYAML(input []byte, options *ProcessorOptions) ([]byte, error) { // Convert short form intrinsic functions (e.g. !Sub) to long form registerTagMarshallers() - data, err := yamlwrapper.YAMLToJSON(input) + // Convert YAML to an object + var yamlObj interface{} + err := yaml.Unmarshal(input, &customTagProcessor{ + target: &yamlObj, + }) if err != nil { return nil, fmt.Errorf("invalid YAML template: %s", err) } - return ProcessJSON(data, options) + data, err := json.Marshal(yamlObj) + if err != nil { + return nil, err + } + return ProcessJSON(data, options) } // ProcessJSON recursively searches through a byte array of JSON data for all diff --git a/intrinsics/tags.go b/intrinsics/tags.go index 151f99983a..80d37d72f0 100644 --- a/intrinsics/tags.go +++ b/intrinsics/tags.go @@ -1,9 +1,8 @@ package intrinsics import ( - "reflect" - - yaml "github.com/sanathkr/go-yaml" + yaml "gopkg.in/yaml.v3" + "strings" ) var allTags = []string{ @@ -12,36 +11,91 @@ var allTags = []string{ "Equals", "Cidr", "And", "If", "Not", "Or", } -type tagUnmarshalerType struct { -} +var tagResolvers = make(map[string]func(*yaml.Node) (*yaml.Node, error)) -func (t *tagUnmarshalerType) UnmarshalYAMLTag(tag string, fieldValue reflect.Value) reflect.Value { +type fragment struct { + content *yaml.Node +} - prefix := "Fn::" - if tag == "Ref" || tag == "Condition" { - prefix = "" - } +func (f *fragment) UnmarshalYAML(value *yaml.Node) error { + var err error + f.content, err = resolveTags(value) + return err +} - tag = prefix + tag +type customTagProcessor struct { + target interface{} +} - output := reflect.ValueOf(make(map[interface{}]interface{})) - key := reflect.ValueOf(tag) +func (i *customTagProcessor) UnmarshalYAML(value *yaml.Node) error { + resolved, err := resolveTags(value) + if err != nil { + return err + } - output.SetMapIndex(key, fieldValue) + return resolved.Decode(i.target) +} - return output +func addTagResolver(tag string, resolver func(*yaml.Node) (*yaml.Node, error)) { + tagResolvers[tag] = resolver } -var tagUnmarshaller = &tagUnmarshalerType{} +func resolveTags(node *yaml.Node) (*yaml.Node, error) { + for tag, fn := range tagResolvers { + if node.Tag == tag { + return fn(node) + } + } -func registerTagMarshallers() { - for _, tag := range allTags { - yaml.RegisterTagUnmarshaler("!"+tag, tagUnmarshaller) + if node.Kind == yaml.SequenceNode || node.Kind == yaml.MappingNode { + var err error + for i := range node.Content { + node.Content[i], err = resolveTags(node.Content[i]) + if err != nil { + return nil, err + } + } } + + return node, nil } -func unregisterTagMarshallers() { +func registerTagMarshallers() { for _, tag := range allTags { - yaml.UnRegisterTagUnmarshaler("!" + tag) + addTagResolver("!"+tag, func(tag string) func(node *yaml.Node) (*yaml.Node, error) { + return func(node *yaml.Node) (*yaml.Node, error) { + prefix := "Fn::" + if tag == "Ref" || tag == "Condition" || strings.HasPrefix(tag, prefix) { + prefix = "" + } + + tag = prefix + tag + + output := &yaml.Node{ + Kind: yaml.MappingNode, + Tag: "!!map", + Value: "", + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Tag: "!!str", Value: tag}, + }, + } + + if len(node.Content) == 0 { + output.Content = append(output.Content, &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: node.Value, + }) + } else { + output.Content = append(output.Content, &yaml.Node{ + Kind: yaml.SequenceNode, + Tag: "!!seq", + Content: node.Content, + }) + } + + return output, nil + } + }(tag)) } } diff --git a/test/yaml/sam-official-samples/inline_swagger/template.yaml b/test/yaml/sam-official-samples/inline_swagger/template.yaml index ca29ec3acf..a7c81a5b24 100644 --- a/test/yaml/sam-official-samples/inline_swagger/template.yaml +++ b/test/yaml/sam-official-samples/inline_swagger/template.yaml @@ -20,7 +20,6 @@ Resources: uri: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyLambdaFunction.Arn}/invocations responses: {} - swagger: '2.0'