Repository: aws-cloudformation/rain (go)

Issue:
scripts/deploy-3p.sh and scripts/deploy-sandbox-pipeline.sh do not work with rain-1.21.0
### Extension Name

n/a

### Describe the bug

attempting to run the scripts will fail with
```
error converting template to map: yaml: unmarshal errors:
  line 0: mapping key "AWSToolsMetrics" already defined at line 4
```

looking at https://github.com/aws-cloudformation/rain/releases/tag/v1.21.0 , https://github.com/aws-cloudformation/rain/pull/627 looks to be the source of this issue.


downgrading to 1.20.1 fixes the issue for me


### Expected behavior

scripts should work

### Reproduction template

n/a

Pull request description:
Fixing duplicate analytics node
Fixes #637

Patch:
diff --git a/cft/pkg/module.go b/cft/pkg/module.go
index 1b8b19cf..1304942b 100644
--- a/cft/pkg/module.go
+++ b/cft/pkg/module.go
@@ -19,7 +19,6 @@ import (
 var HasModules bool
 
 const (
-	Rain                = "Rain"
 	Metadata            = "Metadata"
 	IfParam             = "IfParam"
 	IfNotParam          = "IfNotParam"
diff --git a/cft/pkg/pkg.go b/cft/pkg/pkg.go
index 61d97607..1f62cc62 100644
--- a/cft/pkg/pkg.go
+++ b/cft/pkg/pkg.go
@@ -48,6 +48,9 @@ var Experimental bool
 var NoAnalytics bool
 var HasRainSection bool
 
+const AWSToolsMetrics = "AWSToolsMetrics"
+const Rain = "Rain"
+
 type analytics struct {
 	// Current Rain version
 	Version string
@@ -290,7 +293,10 @@ func Template(t cft.Template, rootDir string, fs *embed.FS) (cft.Template, error
 		if err != nil || metadata == nil {
 			metadata = node.AddMap(retval.Node.Content[0], string(cft.Metadata))
 		}
-		awsToolsMetrics := node.AddMap(metadata, "AWSToolsMetrics")
+		_, awsToolsMetrics, _ := s11n.GetMapValue(metadata, AWSToolsMetrics)
+		if awsToolsMetrics == nil {
+			awsToolsMetrics = node.AddMap(metadata, AWSToolsMetrics)
+		}
 		a := analytics{
 			Version:        config.VERSION,
 			HasModules:     HasModules,
@@ -298,10 +304,17 @@ func Template(t cft.Template, rootDir string, fs *embed.FS) (cft.Template, error
 			HasRainSection: HasRainSection,
 		}
 		s, _ := json.Marshal(&a)
-		awsToolsMetrics.Content = append(awsToolsMetrics.Content,
-			&yaml.Node{Kind: yaml.ScalarNode, Value: "Rain"})
-		awsToolsMetrics.Content = append(awsToolsMetrics.Content,
-			&yaml.Node{Kind: yaml.ScalarNode, Value: string(s)})
+		_, rain, _ := s11n.GetMapValue(awsToolsMetrics, Rain)
+		rainNode := &yaml.Node{Kind: yaml.ScalarNode, Value: string(s)}
+		if rain == nil {
+			config.Debugf("Adding Rain analytics")
+			awsToolsMetrics.Content = append(awsToolsMetrics.Content,
+				&yaml.Node{Kind: yaml.ScalarNode, Value: Rain})
+			awsToolsMetrics.Content = append(awsToolsMetrics.Content, rainNode)
+		} else {
+			config.Debugf("Already had Rain analytics")
+			*rain = *rainNode
+		}
 	}
 
 	return retval, nil
diff --git a/internal/node/node.go b/internal/node/node.go
index 576dab5d..ffca7d8e 100644
--- a/internal/node/node.go
+++ b/internal/node/node.go
@@ -243,7 +243,17 @@ func Add(m *yaml.Node, name string, val string) {
 }
 
 // AddMap adds a new map to the parent node
+// If it already exists, returns the existing map
+// If it doesn't exist, returns the new map
 func AddMap(parent *yaml.Node, name string) *yaml.Node {
+	for i := 0; i < len(parent.Content); i++ {
+		if i%2 != 0 {
+			continue
+		}
+		if parent.Content[i].Value == name {
+			return parent.Content[i+1]
+		}
+	}
 	parent.Content = append(parent.Content,
 		&yaml.Node{Kind: yaml.ScalarNode, Value: name})
 	m := &yaml.Node{Kind: yaml.MappingNode, Content: make([]*yaml.Node, 0)}


Test patch:
diff --git a/internal/node/node_test.go b/internal/node/node_test.go
index a7d00ac9..c95f5d83 100644
--- a/internal/node/node_test.go
+++ b/internal/node/node_test.go
@@ -239,3 +239,15 @@ func TestMergeNodes(t *testing.T) {
 	}
 
 }
+
+func TestAddMap(t *testing.T) {
+	parent := &yaml.Node{Kind: yaml.MappingNode, Content: make([]*yaml.Node, 0)}
+	m := node.AddMap(parent, "Test")
+	if m == nil {
+		t.Errorf("AddMap returned nil")
+	}
+	mm := node.AddMap(parent, "Test")
+	if m != mm {
+		t.Errorf("AddMap second add should return the same object")
+	}
+}
diff --git a/test/templates/analytics2.yaml b/test/templates/analytics2.yaml
new file mode 100644
index 00000000..75102124
--- /dev/null
+++ b/test/templates/analytics2.yaml
@@ -0,0 +1,7 @@
+Metadata:
+  Comment: abc
+  AWSToolsMetrics:
+    AlreadyHere: ForSomeReason
+Resources:
+  Bucket:
+    Type: AWS::S3::Bucket
diff --git a/test/templates/analytics3.yaml b/test/templates/analytics3.yaml
new file mode 100644
index 00000000..b1f5fb8c
--- /dev/null
+++ b/test/templates/analytics3.yaml
@@ -0,0 +1,8 @@
+Metadata:
+  Comment: abc
+  AWSToolsMetrics:
+    AlreadyHere: ForSomeReason
+    Rain: foo
+Resources:
+  Bucket:
+    Type: AWS::S3::Bucket