internal/pkg/template/diff/override.go (188 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package diff
import (
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
// overrider overrides the parsing behavior between two yaml nodes under certain keys.
type overrider interface {
match(from, to *yaml.Node, key string, overrider overrider) bool
parse(from, to *yaml.Node, key string, overrider overrider) (diffNode, error)
}
type ignoreSegment struct {
key string
next *ignoreSegment
}
// ignorer ignores the diff between two yaml nodes under specified key paths.
type ignorer struct {
curr *ignoreSegment
}
// match returns true if the difference between the from and to at the key should be ignored.
func (m *ignorer) match(_, _ *yaml.Node, key string, _ overrider) bool {
if key != m.curr.key {
return false
}
if m.curr.next == nil {
return true
}
m.curr = m.curr.next
return false
}
// Parse is a no-op for an ignorer.
func (m *ignorer) parse(_, _ *yaml.Node, _ string, _ overrider) (diffNode, error) {
return nil, nil
}
// Check https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html for
// a complete list of intrinsic functions. Some are not included here as they do not need an overrider.
var (
exists = struct{}{}
intrinsicFunctionFullNames = map[string]struct{}{
"Ref": exists,
"Fn::Base64": exists,
"Fn::Cidr": exists,
"Fn::FindInMap": exists,
"Fn::GetAtt": exists,
"Fn::GetAZs": exists,
"Fn::ImportValue": exists,
"Fn::Join": exists,
"Fn::Select": exists,
"Fn::Split": exists,
"Fn::Sub": exists,
"Fn::Transform": exists,
// Condition functions.
"Condition": exists,
"Fn::And": exists,
"Fn::Equals": exists,
"Fn::If": exists,
"Fn::Not": exists,
"Fn::Or": exists,
}
intrinsicFunctionShortNames = map[string]struct{}{
"!Ref": exists,
"!Base64": exists,
"!Cidr": exists,
"!FindInMap": exists,
"!GetAtt": exists,
"!GetAZs": exists,
"!ImportValue": exists,
"!Join": exists,
"!Select": exists,
"!Split": exists,
"!Sub": exists,
"Transform": exists,
// Condition functions.
"!Condition": exists,
"!And": exists,
"!Equals": exists,
"!If": exists,
"!Not": exists,
"!Or": exists,
}
)
// intrinsicFuncMatcher matches intrinsic function nodes.
type intrinsicFuncMatcher struct{}
// match returns true if from and to node represent the same intrinsic function.
// Example1: "!Ref" and "Ref:" will return true.
// Example2: "!Ref" and "!Ref" will return true.
// Example3: "!Ref" and "Fn::GetAtt:" will return false because they are different intrinsic functions.
// Example4: "!Magic" and "Fn::Magic" will return false because they are not intrinsic functions.
func (_ *intrinsicFuncMatcher) match(from, to *yaml.Node, _ string, _ overrider) bool {
if from == nil || to == nil {
return false
}
fromFunc, toFunc := intrinsicFuncName(from), intrinsicFuncName(to)
return fromFunc != "" && toFunc != "" && fromFunc == toFunc
}
// intrinsicFuncMatcher matches and parses two intrinsic function nodes written in different form (full/short).
type intrinsicFuncMapTagConverter struct {
intrinsicFunc intrinsicFuncMatcher
}
// match returns true if from and to node represent the same intrinsic function written in different (full/short) form.
// Example1: "!Ref" and "Ref:" will return true.
// Example2: "!Ref" and "!Ref" will return false because they are written in the same form (i.e. short).
// Example3: "!Ref" and "Fn::GetAtt:" will return false because they are different intrinsic functions.
// For more on intrinsic functions and full/short forms, read https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ToJsonString.html.
func (converter *intrinsicFuncMapTagConverter) match(from, to *yaml.Node, key string, overrider overrider) bool {
if !converter.intrinsicFunc.match(from, to, key, overrider) {
return false
}
// Exactly one of from and to is full form.
return (from.Kind == yaml.MappingNode || to.Kind == yaml.MappingNode) && (from.Kind != to.Kind)
}
// parse compares two intrinsic function nodes written in different form (full vs. short).
// When the inputs to the intrinsic functions have different data types, parse assumes that no type conversion is needed
// for correct comparison.
// E.g. given "!Func: [1,2]" and "Fn::Func: '1,2'", parse assumes that comparing [1,2] with "1,2" produces the desired result.
// Note that this does not hold for "GetAtt" function: "!GetAtt: [1,2]" and "!GetAtt: 1.2" should be considered the same.
// parse assumes that from and to are matched by intrinsicFuncMapTagConverter.
func (*intrinsicFuncMapTagConverter) parse(from, to *yaml.Node, key string, overrider overrider) (diffNode, error) {
var diff diffNode
var err error
if from.Kind == yaml.MappingNode {
// The full form mapping node always contain only one child node. The second element in `Content` is the
// value of the child node. Read https://www.efekarakus.com/2020/05/30/deep-dive-go-yaml-cfn.html.
diff, err = parse(from.Content[1], stripTag(to), from.Content[0].Value, overrider)
} else {
diff, err = parse(stripTag(from), to.Content[1], to.Content[0].Value, overrider)
}
if diff == nil {
return nil, err
}
return &keyNode{
keyValue: key,
childNodes: []diffNode{diff},
}, nil
}
// getAttConverter matches and parses two YAML nodes that calls the intrinsic function "GetAtt".
// Unlike intrinsicFuncMapTagConverter, getAttConverter does not require "from" and "to" to be written in different form.
// The input to "GetAtt" could be either a sequence or a scalar. All the followings are valid and should be considered equal.
// Fn::GetAtt: LogicalID.Att.SubAtt, Fn::GetAtt: [LogicalID, Att.SubAtt], !GetAtt LogicalID.Att.SubAtt, !GetAtt [LogicalID, Att.SubAtt].
type getAttConverter struct {
intrinsicFuncMapTagConverter
}
// match returns true if both from node and to node are calling the "GetAtt" intrinsic function.
// "GetAtt" only accepts either sequence or scalar, therefore match returns false if either of from and to has invalid
// input node to "GetAtt".
// Example1: "!GetAtt" and "!GetAtt" returns true.
// Example2: "!GetAtt" and "Fn::GetAtt" returns true.
// Example3: "!Ref" and "!GetAtt" returns false.
// Example4: "!GetAtt [a,b]" and "Fn::GetAtt: a:b" returns false because the input type is wrong.
func (converter *getAttConverter) match(from, to *yaml.Node, key string, overrider overrider) bool {
if !converter.intrinsicFunc.match(from, to, key, overrider) {
return false
}
if intrinsicFuncName(from) != "GetAtt" {
return false
}
fromValue, toValue := from, to
if from.Kind == yaml.MappingNode {
// A valid full-form intrinsic function always contain a child node.
// This must be valid because it has passed `converter.intrinsicFunc.match`.
fromValue = from.Content[1]
}
if to.Kind == yaml.MappingNode {
toValue = to.Content[1]
}
return (fromValue.Kind == yaml.ScalarNode || fromValue.Kind == yaml.SequenceNode) && (toValue.Kind == yaml.ScalarNode || toValue.Kind == yaml.SequenceNode)
}
// parse compares two nodes that call the "GetAtt" function. Both from and to can be written in either full or short form.
// parse assumes that from and to are already matched by getAttConverter.
func (converter *getAttConverter) parse(from, to *yaml.Node, key string, overrider overrider) (diffNode, error) {
// Extract the input node to GetAtt.
fromValue, toValue := from, to
if from.Kind == yaml.MappingNode {
fromValue = from.Content[1] // A valid full-form intrinsic function always contain a child node.
}
if to.Kind == yaml.MappingNode {
toValue = to.Content[1]
}
// If the input node are of the same type (i.e. both seq or both scalar), parse them normally.
// Otherwise, first convert the scalar input to seq input, then parse.
if fromValue.Kind != toValue.Kind {
var err error
switch {
case fromValue.Kind == yaml.ScalarNode:
fromValue, err = getAttScalarToSeq(fromValue)
case toValue.Kind == yaml.ScalarNode:
toValue, err = getAttScalarToSeq(toValue)
}
if err != nil {
return nil, err
}
}
diff, err := parse(stripTag(fromValue), stripTag(toValue), "Fn::GetAtt", overrider)
if diff == nil {
return nil, err
}
return &keyNode{
keyValue: key,
childNodes: []diffNode{diff},
}, nil
}
// intrinsicFuncName returns the name ofo the intrinsic function given a node.
// If the node is not an intrinsic function node, it returns an empty string.
func intrinsicFuncName(node *yaml.Node) string {
if node.Kind != yaml.MappingNode {
if _, ok := intrinsicFunctionShortNames[node.Tag]; !ok {
return ""
}
return strings.TrimPrefix(node.Tag, "!")
}
if len(node.Content) != 2 {
// The full form mapping node always contain only one child node, whose key is the func name in full form.
// Read https://www.efekarakus.com/2020/05/30/deep-dive-go-yaml-cfn.html.
return ""
}
if _, ok := intrinsicFunctionFullNames[node.Content[0].Value]; !ok {
return ""
}
return strings.TrimPrefix(node.Content[0].Value, "Fn::")
}
func stripTag(node *yaml.Node) *yaml.Node {
return &yaml.Node{
Kind: node.Kind,
Style: node.Style,
Content: node.Content,
Value: node.Value,
}
}
// Transform scalar node "LogicalID.Attr" to sequence node [LogicalID, Attr].
func getAttScalarToSeq(scalarNode *yaml.Node) (*yaml.Node, error) {
split := strings.SplitN(scalarNode.Value, ".", 2) // split has at least one element in it.
var seqFromScalar yaml.Node
if err := yaml.Unmarshal([]byte(fmt.Sprintf("[%s]", strings.Join(split, ","))), &seqFromScalar); err != nil {
return nil, err
}
if len(seqFromScalar.Content) == 0 {
return nil, nil
}
return seqFromScalar.Content[0], nil
}