@@ -2,7 +2,10 @@ package cft
22
33import (
44 "errors"
5+ "fmt"
6+ "strings"
57
8+ "github.com/aws-cloudformation/rain/internal/config"
69 "github.com/aws-cloudformation/rain/internal/node"
710 "github.com/aws-cloudformation/rain/internal/s11n"
811 "gopkg.in/yaml.v3"
@@ -43,6 +46,9 @@ type ModuleConfig struct {
4346
4447 // The root directory of the template that configures this module
4548 ParentRootDir string
49+
50+ // If this module is wrapped in Fn::ForEach, this will be populated
51+ FnForEach * FnForEach
4652}
4753
4854func (c * ModuleConfig ) Properties () map [string ]any {
@@ -53,7 +59,8 @@ func (c *ModuleConfig) Overrides() map[string]any {
5359 return node .DecodeMap (c .OverridesNode )
5460}
5561
56- // ResourceOverridesNode returns the Overrides node for the given resource if it exists
62+ // ResourceOverridesNode returns the Overrides node for the
63+ // given resource if it exists
5764func (c * ModuleConfig ) ResourceOverridesNode (name string ) * yaml.Node {
5865 if c .OverridesNode == nil {
5966 return nil
@@ -66,19 +73,59 @@ const (
6673 Source string = "Source"
6774 Properties string = "Properties"
6875 Overrides string = "Overrides"
69- Map string = "Map "
76+ ForEach string = "Fn::ForEach "
7077)
7178
7279// parseModuleConfig parses a single module configuration
7380// from the Modules section in the template
74- func (t * Template ) ParseModuleConfig (name string , n * yaml.Node ) (* ModuleConfig , error ) {
75- if n .Kind != yaml .MappingNode {
76- return nil , errors .New ("not a mapping node" )
77- }
81+ func (t * Template ) ParseModuleConfig (
82+ name string , n * yaml.Node ) (* ModuleConfig , error ) {
83+
7884 m := & ModuleConfig {}
7985 m .Name = name
8086 m .Node = n
8187
88+ // Handle Fn::ForEach modules
89+ if strings .HasPrefix (name , ForEach ) && n .Kind == yaml .SequenceNode {
90+ if len (n .Content ) != 3 {
91+ msg := "expected %s len 3, got %d"
92+ return nil , fmt .Errorf (msg , name , len (n .Content ))
93+ }
94+
95+ m .FnForEach = & FnForEach {}
96+
97+ loopName := strings .Replace (name , ForEach , "" , 1 )
98+ loopName = strings .Replace (loopName , ":" , "" , - 1 )
99+ m .FnForEach .LoopName = loopName
100+
101+ m .Name = loopName // TODO: ?
102+
103+ m .FnForEach .Identifier = n .Content [0 ].Value
104+ m .FnForEach .Collection = n .Content [1 ]
105+ outputKeyValue := n .Content [2 ]
106+
107+ if outputKeyValue .Kind != yaml .MappingNode ||
108+ len (outputKeyValue .Content ) != 2 ||
109+ outputKeyValue .Content [1 ].Kind != yaml .MappingNode {
110+ msg := "invalid %s, expected OutputKey: OutputValue mapping"
111+ return nil , fmt .Errorf (msg , name )
112+ }
113+
114+ m .FnForEach .OutputKey = outputKeyValue .Content [0 ].Value
115+ m .Node = outputKeyValue .Content [1 ]
116+ m .FnForEach .OutputValue = m .Node
117+ n = m .Node
118+ m .Map = m .FnForEach .Collection
119+
120+ config .Debugf ("ModuleConfig.FnForEach: %+v" , m .FnForEach )
121+
122+ }
123+
124+ if n .Kind != yaml .MappingNode {
125+ config .Debugf ("ParseModuleConfig %s: %s" , name , node .ToSJson (n ))
126+ return nil , errors .New ("not a mapping node" )
127+ }
128+
82129 content := n .Content
83130 for i := 0 ; i < len (content ); i += 2 {
84131 attr := content [i ].Value
@@ -90,30 +137,41 @@ func (t *Template) ParseModuleConfig(name string, n *yaml.Node) (*ModuleConfig,
90137 m .PropertiesNode = val
91138 case Overrides :
92139 m .OverridesNode = val
93- case Map :
140+ case "ForEach" :
94141 m .Map = val
95142 }
96143 }
97144
98- //err := t.ValidateModuleConfig(m)
99- //if err != nil {
100- // return nil, err
101- //}
102-
103145 return m , nil
104146}
105147
106- // ValidateModuleConfig makes sure the configuration does not
107- // break any rules, such as not having a Property with the
108- // same name as a Parameter.
109- //func (t *Template) ValidateModuleConfig(moduleConfig *ModuleConfig) error {
110- // props := moduleConfig.Properties()
111- // for key := range props {
112- // _, err := t.GetParameter(key)
113- // if err == nil {
114- // return fmt.Errorf("module %s in %s has Property %s with the same name as a template Parameter",
115- // moduleConfig.Name, moduleConfig.ParentRootDir, key)
116- // }
117- // }
118- // return nil
119- //}
148+ type FnForEach struct {
149+ LoopName string
150+ Identifier string
151+ Collection * yaml.Node
152+ OutputKey string
153+ OutputValue * yaml.Node
154+ }
155+
156+ // OutputKeyHasIdentifier returns true if the key uses the identifier
157+ func (ff * FnForEach ) OutputKeyHasIdentifier () bool {
158+ dollar := "${" + ff .Identifier + "}"
159+ amper := "&{" + ff .Identifier + "}"
160+ if strings .Contains (ff .OutputKey , dollar ) {
161+ return true
162+ }
163+ if strings .Contains (ff .OutputKey , amper ) {
164+ return true
165+ }
166+ return false
167+ }
168+
169+ // ReplaceIdentifier replaces instance of the identifier in s for collection
170+ // key k
171+ func ReplaceIdentifier (s , k , identifier string ) string {
172+ dollar := "${" + identifier + "}"
173+ amper := "&{" + identifier + "}"
174+ s = strings .Replace (s , dollar , k , - 1 )
175+ s = strings .Replace (s , amper , k , - 1 )
176+ return s
177+ }
0 commit comments