Skip to content

Commit 251173d

Browse files
authored
Merge pull request #676 from ericzbeard/modules
Add features to Modules
2 parents ea4eb87 + df87652 commit 251173d

File tree

153 files changed

+4912
-2686
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+4912
-2686
lines changed

README.md

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -457,13 +457,14 @@ Modules are not allowed to refer to themselves directly or in cycles. Module A
457457
can’t import Module A, and it can’t import Module B if that module imports
458458
Module A.
459459
460-
Modules support a basic form of looping/foreach by adding a `Map` attribute to
461-
the module configuration. Special variables `$MapIndex` and `$MapValue` can be
462-
used to refer to the index and list value. Logical Ids are auto-incremented by
460+
Modules support a basic form of looping/foreach by either using the familiar
461+
`Fn::ForEach` syntax, or with a shorthand by adding a `ForEach` attribute to
462+
the module configuration. Special variables `$Identifier` and `$Index` can be
463+
used to refer to the value and list index. With the shorthand, or if you don't
464+
put the Identifier in the logical id, logical ids are auto-incremented by
463465
adding an integer starting at zero. Since this is a client-side-only feature,
464466
list values must be fully resolved scalars, not values that must be resolved at
465-
deploy time. When deploy-time values are needed, `Fn::ForEach` is a better
466-
design option. Local modules do not process `Fn::ForEach`.
467+
deploy time.
467468
468469
```
469470
Parameters:
@@ -474,7 +475,7 @@ Parameters:
474475
Modules:
475476
Content:
476477
Source: ./map-module.yaml
477-
Map: !Ref List
478+
ForEach: !Ref List
478479
Properties:
479480
Name: !Sub my-bucket-$MapValue
480481
```
@@ -506,15 +507,12 @@ It’s also possible to refer to elements within a `Map` using something like
506507
which resolves to a list of all of the `Arn` outputs from that module.
507508
508509
When a module is processed, the first thing that happens is parsing of the
509-
`Conditions` within the module. These conditions must be fully resolvable
510-
client-side, since the package command does not have access to Parameters or
511-
deploy-time values. These conditions are converted to a dictionary of boolean
512-
values and the `Conditions` section is not emitted into the parent template. It
513-
is not merged into the parent. Any resources marked with a false condition are
514-
removed, and any property nodes with conditions are processed. Any values of
515-
`!Ref AWS::NoValue` are removed. No evidence of conditions will remain in the
516-
markup that is merged into the parent template, unless the condition is not
517-
found in the module.
510+
Conditions within the module. Any Resources, Modules, or Outputs marked with a
511+
false condition are removed, and any property nodes with conditions are
512+
processed. Any values of !Ref AWS::NoValue are removed. Any unresolved
513+
conditions (for example, a condition that references a paramter in the parent
514+
template, or something like AWS::Region) are emitted into the parent template,
515+
prefixed with the module name.
518516
519517
Much of the value of module output is in the smart handling of `Ref`,
520518
`Fn::GetAtt`, and `Fn::Sub`. For the most part, we want these to “just work” in
@@ -612,11 +610,6 @@ Modules:
612610
Source: $def/a/b/bar.yaml
613611
```
614612
615-
616-
617-
618-
619-
620613
### Publish modules to CodeArtifact
621614
622615
Rain integrates with AWS CodeArtifact to enable an experience similar to npm

cft/cft.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ func (t *Template) AddMappedModule(copiedConfig *ModuleConfig) {
257257
t.ModuleMaps = make(map[string]*ModuleConfig)
258258
}
259259
t.ModuleMaps[copiedConfig.Name] = copiedConfig
260+
keyName := copiedConfig.OriginalName + copiedConfig.MapKey
261+
// Also add the name if referenced by key
262+
t.ModuleMaps[keyName] = copiedConfig
263+
260264
if t.ModuleMapNames == nil {
261265
t.ModuleMapNames = make(map[string][]string)
262266
}
@@ -282,3 +286,9 @@ func (t *Template) AddResolvedModuleNode(n *yaml.Node) {
282286
func (t *Template) ModuleAlreadyResolved(n *yaml.Node) bool {
283287
return slices.Contains(t.ModuleResolved, n)
284288
}
289+
290+
// IsRef returns true if the node is a 2-length Mapping node
291+
// that starts with "Ref"
292+
func IsRef(n *yaml.Node) bool {
293+
return len(n.Content) == 2 && n.Content[0].Value == string(Ref)
294+
}

cft/module_config.go

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package cft
22

33
import (
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

4854
func (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
5764
func (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

Comments
 (0)