Skip to content

Commit

Permalink
Merge pull request #577 from ericzbeard/fix-572
Browse files Browse the repository at this point in the history
Fix bugs in Constants and Sub processing
  • Loading branch information
ericzbeard authored Oct 31, 2024
2 parents d6fcc5d + baf9606 commit 7cbaaab
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 76 deletions.
2 changes: 1 addition & 1 deletion cft/graph/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func getRefs(t map[string]interface{}) []string {
}

func parseSubString(refs []string, substr string) []string {
words, err := parse.ParseSub(substr)
words, err := parse.ParseSub(substr, false)
if err != nil {
config.Debugf("Unable to parse Sub %s: %v", substr, err)
return refs
Expand Down
8 changes: 7 additions & 1 deletion cft/parse/parse_sub.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const (
// SubWord { T: STR, W: "-123" }
//
// Invalid syntax like "${AAA" returns an error
func ParseSub(sub string) ([]SubWord, error) {
func ParseSub(sub string, leaveBang bool) ([]SubWord, error) {
words := make([]SubWord, 0)
state := READSTR
var last rune
Expand Down Expand Up @@ -111,6 +111,12 @@ func ParseSub(sub string) ([]SubWord, error) {
if state == READLIT {
// Don't write the ! to the string
state = READSTR
if leaveBang {
// Unless we actually want it
// The cc command doesn't want it
// The Rain Constants parser wants it
buf += string(r)
}
} else {
// This is a ! somewhere not related to a LITERAL
buf += string(r)
Expand Down
6 changes: 3 additions & 3 deletions cft/parse/parse_sub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestParseSub(t *testing.T) {
}

for sub, expect := range cases {
words, err := parse.ParseSub(sub)
words, err := parse.ParseSub(sub, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -65,14 +65,14 @@ func TestParseSub(t *testing.T) {

// Invalid strings should fail
sub := "${AAA"
words, err := parse.ParseSub(sub)
words, err := parse.ParseSub(sub, false)
if err == nil {
t.Fatalf("\"%s\": should have failed, got %v", sub, words)
}

// Rain constants
sub = "${Rain::Test}"
words, err = parse.ParseSub(sub)
words, err = parse.ParseSub(sub, false)
if err != nil {
t.Fatal(err)
}
Expand Down
94 changes: 94 additions & 0 deletions cft/pkg/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package pkg
import (
"fmt"

"github.com/aws-cloudformation/rain/cft/parse"
"github.com/aws-cloudformation/rain/cft/visitor"
"github.com/aws-cloudformation/rain/internal/config"
"github.com/aws-cloudformation/rain/internal/node"
"gopkg.in/yaml.v3"
)

// rainConstant parses a !Rain::Constant node
Expand All @@ -25,3 +28,94 @@ func rainConstant(ctx *directiveContext) (bool, error) {

return true, nil
}

// replaceConstants replaces ${Rain::ConstantName} in a single scalar node
// If the constant name is not found in the map created from the Rain section
// In the template, an error is returned
func replaceConstants(n *yaml.Node, constants map[string]*yaml.Node) error {
if n.Kind != yaml.ScalarNode {
return fmt.Errorf("expected n to be a ScalarNode")
}

// Parse every scalar as if it was a Sub. Look for ${Rain::X}

retval := ""
words, err := parse.ParseSub(n.Value, true)
if err != nil {
return err
}
for _, w := range words {
switch w.T {
case parse.STR:
retval += w.W
case parse.REF:
retval += fmt.Sprintf("${%s}", w.W)
case parse.AWS:
retval += fmt.Sprintf("${AWS::%s}", w.W)
case parse.GETATT:
retval += fmt.Sprintf("${%s}", w.W)
case parse.RAIN:
val, ok := constants[w.W]
if !ok {
return fmt.Errorf("did not find Rain constant %s", w.W)
}
retval += val.Value
}
}

config.Debugf("Replacing %s with %s", n.Value, retval)
n.Value = retval

return nil
}

func IsSubNeeded(s string) bool {

words, err := parse.ParseSub(s, true)
if err != nil {
config.Debugf("error in IsSubNeeded: %v", err)
return true
}
for _, w := range words {
switch w.T {
case parse.STR:
// Ignore this
default:
// Anything else means it's needed
return true
}
}
return false
}

// replaceTemplateConstants scans the entire template looking for Sub strings
// and replaces all instances of ${Rain::ConstantName} if that name exists
// in the Rain/Constants section of the template
func replaceTemplateConstants(templateNode *yaml.Node, constants map[string]*yaml.Node) {

config.Debugf("Constants: %v", constants)

vf := func(n *visitor.Visitor) {
yamlNode := n.GetYamlNode()
if yamlNode.Kind == yaml.MappingNode {
if len(yamlNode.Content) == 2 && yamlNode.Content[0].Value == "Fn::Sub" {
config.Debugf("About to replace constants in %s", yamlNode.Content[1].Value)
err := replaceConstants(yamlNode.Content[1], constants)
if err != nil {
config.Debugf("%v", err)
}

// Remove unnecessary Subs
// Parse the value again and see if it has any non-words
if !IsSubNeeded(yamlNode.Content[1].Value) {
config.Debugf("Sub is not needed for %s", yamlNode.Content[1].Value)
*yamlNode = yaml.Node{Kind: yaml.ScalarNode, Value: yamlNode.Content[1].Value}
}

}
}
}

visitor := visitor.NewVisitor(templateNode)
visitor.Visit(vf)
}
25 changes: 23 additions & 2 deletions cft/pkg/constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Resources:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "pre-${Prefix}-${Rain::Test1}-suffix"
Foo: !Sub ${Rain::Test1}
Bar: !Sub ${!leavemealone}
`
expect := `
Parameters:
Expand All @@ -50,8 +52,9 @@ Resources:
Bucket3:
Type: AWS::S3::Bucket
Properties:
BucketName:
Fn::Sub: pre-${Prefix}-ezbeard-rain-test-constants-suffix
BucketName: !Sub pre-${Prefix}-ezbeard-rain-test-constants-suffix
Foo: ezbeard-rain-test-constants
Bar: ${!leavemealone}
`

//config.Debug = true
Expand Down Expand Up @@ -90,3 +93,21 @@ func TestReplaceConstants(t *testing.T) {
t.Fatalf("Expected Foo, got %s", n.Value)
}
}

func TestIsSubNeeded(t *testing.T) {
cases := make(map[string]bool)
cases["ABC"] = false
cases["${A}bc"] = true
cases["${Rain::Something}"] = true
cases[""] = false
cases["${Abc.Def"] = true
cases["${!saml:sub}"] = false
cases["${!Literal}-abc"] = false
cases["$foo$bar"] = false

for k, v := range cases {
if IsSubNeeded(k) != v {
t.Errorf("IsSubNeeded(%s) should be %v", k, v)
}
}
}
2 changes: 1 addition & 1 deletion cft/pkg/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ func resolveModuleSub(parentName string, prop *yaml.Node, sidx int, ctx *refctx)

refFoundInParams := false

words, err := parse.ParseSub(prop.Value)
words, err := parse.ParseSub(prop.Value, true)
if err != nil {
return err
}
Expand Down
72 changes: 6 additions & 66 deletions cft/pkg/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type transformContext struct {
func transform(ctx *transformContext) (bool, error) {
changed := false

config.Debugf("transform: %s", node.ToSJson(ctx.nodeToTransform))
// config.Debugf("transform: %s", node.ToSJson(ctx.nodeToTransform))

// registry is a map of functions defined in rain.go
for path, fn := range registry {
Expand All @@ -79,70 +79,6 @@ func transform(ctx *transformContext) (bool, error) {
return changed, nil
}

// replaceConstants replaces ${Rain::ConstantName} in a single scalar node
// If the constant name is not found in the map created from the Rain section
// In the template, an error is returned
func replaceConstants(n *yaml.Node, constants map[string]*yaml.Node) error {
if n.Kind != yaml.ScalarNode {
return fmt.Errorf("expected n to be a ScalarNode")
}

// Parse every scalar as if it was a Sub. Look for ${Rain::X}

retval := ""
words, err := parse.ParseSub(n.Value)
if err != nil {
return err
}
for _, w := range words {
switch w.T {
case parse.STR:
retval += w.W
case parse.REF:
retval += fmt.Sprintf("${%s}", w.W)
case parse.AWS:
retval += fmt.Sprintf("${AWS::%s}", w.W)
case parse.GETATT:
retval += fmt.Sprintf("${%s}", w.W)
case parse.RAIN:
val, ok := constants[w.W]
if !ok {
return fmt.Errorf("did not find Rain constant %s", w.W)
}
retval += val.Value
}
}

config.Debugf("Replacing %s with %s", n.Value, retval)
n.Value = retval

return nil
}

// replaceTemplateConstants scans the entire template looking for Sub strings
// and replaces all instances of ${Rain::ConstantName} if that name exists
// in the Rain/Constants section of the template
func replaceTemplateConstants(templateNode *yaml.Node, constants map[string]*yaml.Node) {

config.Debugf("Constants: %v", constants)

vf := func(n *visitor.Visitor) {
yamlNode := n.GetYamlNode()
if yamlNode.Kind == yaml.MappingNode {
if len(yamlNode.Content) == 2 && yamlNode.Content[0].Value == "Fn::Sub" {
config.Debugf("About to replace constants in %s", yamlNode.Content[1].Value)
err := replaceConstants(yamlNode.Content[1], constants)
if err != nil {
config.Debugf("%v", err)
}
}
}
}

visitor := visitor.NewVisitor(templateNode)
visitor.Visit(vf)
}

// Template returns t with assets included as per AWS CLI packaging rules
// and any Rain:: functions used.
// rootDir must be passed in so that any included assets can be loaded from the same directory
Expand All @@ -156,9 +92,11 @@ func Template(t cft.Template, rootDir string, fs *embed.FS) (cft.Template, error
// First look for a Rain section and store constants
t.Constants = make(map[string]*yaml.Node)
rainNode, err := t.GetSection(cft.Rain)
hasRainNode := false
if err != nil {
config.Debugf("Unable to get Rain section: %v", err)
} else {
hasRainNode = true
config.Debugf("Rain node: %s", node.ToSJson(rainNode))
_, c, _ := s11n.GetMapValue(rainNode, "Constants")
if c != nil {
Expand Down Expand Up @@ -260,7 +198,9 @@ func Template(t cft.Template, rootDir string, fs *embed.FS) (cft.Template, error
v.Visit(replaceAnchors)

// Look for ${Rain::ConstantName} in all Sub strings
replaceTemplateConstants(templateNode, constants)
if hasRainNode {
replaceTemplateConstants(templateNode, constants)
}

// Marshal and Unmarshal to resolve new line/column numbers

Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/cc/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ func resolveSub(n *yaml.Node, resource *Resource) (string, error) {
// Ref for single strings like ${MyParam} or ${MyBucket}
// Map values
// ${!Literal}
words, err := parse.ParseSub(n.Value)
words, err := parse.ParseSub(n.Value, false)
if err != nil {
return "", err
}
Expand All @@ -377,7 +377,7 @@ func resolveSub(n *yaml.Node, resource *Resource) (string, error) {
if n.Content[1].Kind != yaml.MappingNode {
return "", fmt.Errorf("expected Sub %s Content[1] to be a Mapping", sub)
}
words, err := parse.ParseSub(sub)
words, err := parse.ParseSub(sub, false)
if err != nil {
return "", err
}
Expand Down
2 changes: 2 additions & 0 deletions test/templates/constants.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ Resources:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "pre-${Prefix}-${Rain::Test1}-suffix"
Foo: !Sub ${Rain::Test1}
Bar: !Sub ${!leavemealone}

0 comments on commit 7cbaaab

Please sign in to comment.