Repository: aws-cloudformation/rain
Pull request #349 (2024-04-03 17:28:30)

Pkl from yaml
Improved generation of Pkl files from YAML. You can output a basic Pkl file with no module imports, or a higher level representation using classes from the CloudFormation Pkl package.

Also fixed an unrelated nil ref related to existing parameter values in `deploy`.



Patch:
diff --git a/cft/format/pkl.go b/cft/format/pkl.go
index 0af0c21a..d3f1cbdb 100644
--- a/cft/format/pkl.go
+++ b/cft/format/pkl.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"slices"
+	"strconv"
 	"strings"
 
 	"github.com/aws-cloudformation/rain/cft"
@@ -12,8 +13,6 @@ import (
 	"gopkg.in/yaml.v3"
 )
 
-var PklPackageAlias string = "@cfn"
-
 func getClassName(resource *yaml.Node) string {
 	typeName := getTypeName(resource)
 	tokens := strings.Split(typeName, "::")
@@ -64,7 +63,7 @@ func getModuleNameFromPath(path string) string {
 	return tokens[2]
 }
 
-func writeResource(sb *strings.Builder, name string, resource *yaml.Node) error {
+func writeResource(sb *strings.Builder, name string, resource *yaml.Node, basic bool) error {
 	indent := "    "
 	modulePath := getModulePath(resource)
 	moduleName := getModuleNameFromPath(modulePath)
@@ -74,17 +73,21 @@ func writeResource(sb *strings.Builder, name string, resource *yaml.Node) error
 	// the resource in the verbose way, without using classes.
 	// This is likely a 3p resource that does not appear in the registry
 
-	// TODO: Make "raw" an arg so we can output CloudFormation YAML with no imports
-
 	if moduleName == "" {
+		basic = true
+	}
+
+	// ["LogicalId"] =
+	if basic {
 		w(sb, "%s[\"%s\"] {\n", indent, name)
 	} else {
 		w(sb, "%s[\"%s\"] = new %s.%s {\n", indent, name, moduleName, className)
 	}
+
 	for i := 0; i < len(resource.Content); i += 2 {
 		attrName := resource.Content[i].Value
 		attr := resource.Content[i+1]
-		if attrName == "Properties" && moduleName != "" {
+		if attrName == "Properties" && !basic {
 			// In our generated classes we push all properties up to the top level
 			// for convenience and type safety. (There are a few clashes we have to handle)
 			for j := 0; j < len(attr.Content); j += 2 {
@@ -94,29 +97,34 @@ func writeResource(sb *strings.Builder, name string, resource *yaml.Node) error
 				switch prop.Kind {
 				case yaml.ScalarNode:
 					sb.WriteString(" = ")
-					writeNode(sb, prop, "")
+					writeNode(sb, prop, "", basic)
 				case yaml.SequenceNode:
 					fallthrough
 				case yaml.MappingNode:
-					// TODO: Need to modify the writes so we use classes instead of maps
-					// Are going to need reflection for this? Or can we predict class names?
-					sb.WriteString(" = new Mapping {\n")
-					writeNode(sb, prop, indent+"        ")
-					sb.WriteString(indent + "    }\n")
+					if isIntrinsic(prop.Content[0].Value) {
+						w(sb, " = ")
+						writeNode(sb, prop, indent+"        ", basic)
+						w(sb, "\n")
+					} else {
+						w(sb, " {\n")
+						writeNode(sb, prop, indent+"        ", basic)
+						sb.WriteString(indent + "    }\n")
+					}
 				}
 			}
 
 		} else {
+			// Basic rendering, without using module classes
 			w(sb, "    %s%s", indent, attrName)
 			switch attr.Kind {
 			case yaml.ScalarNode:
 				sb.WriteString(" = ")
-				writeNode(sb, attr, "")
+				writeNode(sb, attr, "", basic)
 			case yaml.SequenceNode:
 				fallthrough
 			case yaml.MappingNode:
 				sb.WriteString(" {\n")
-				writeNode(sb, attr, indent+"        ")
+				writeNode(sb, attr, indent+"        ", basic)
 				sb.WriteString(indent + "    }\n")
 			}
 		}
@@ -173,24 +181,54 @@ func w(sb *strings.Builder, f string, args ...any) {
 }
 
 // writeSequence writes a generic sequence
-func writeSequence(sb *strings.Builder, n *yaml.Node, indent string) error {
+func writeSequence(sb *strings.Builder, n *yaml.Node, indent string, basic bool) error {
 	for i := 0; i < len(n.Content); i++ {
 		switch n.Content[i].Kind {
 		case yaml.ScalarNode:
-			writeNode(sb, n.Content[i], indent)
+			writeNode(sb, n.Content[i], indent, basic)
 		case yaml.MappingNode:
-			sb.WriteString(indent + " new {\n")
-			writeNode(sb, n.Content[i], indent+"    ")
-			sb.WriteString(indent + "}\n")
+			if !basic && isIntrinsic(n.Content[i].Content[0].Value) {
+				w(sb, indent)
+				writeNode(sb, n.Content[i], indent+"        ", basic)
+			} else {
+				sb.WriteString(indent + " new {\n")
+				writeNode(sb, n.Content[i], indent+"    ", basic)
+				sb.WriteString(indent + "}\n")
+			}
 		case yaml.SequenceNode:
-			writeNode(sb, n.Content[i], indent)
+			writeNode(sb, n.Content[i], indent, basic)
 		}
 	}
 	return nil
 }
 
+const (
+	SUB       = "Fn::Sub"
+	REF       = "Ref"
+	GETATT    = "Fn::GetAtt"
+	EQUALS    = "Fn::Equals"
+	CONTAINS  = "Fn::Contains"
+	FINDINMAP = "Fn::FindInMap"
+	GETAZS    = "Fn::GetAZs"
+	SELECT    = "Fn::Select"
+)
+
+func isIntrinsic(name string) bool {
+	intrinsics := []string{
+		SUB,
+		REF,
+		GETATT,
+		EQUALS,
+		CONTAINS,
+		FINDINMAP,
+		GETAZS,
+		SELECT,
+	}
+	return slices.Contains(intrinsics, name)
+}
+
 // writeMap writes out a generic Mapping
-func writeMap(sb *strings.Builder, n *yaml.Node, indent string) error {
+func writeMap(sb *strings.Builder, n *yaml.Node, indent string, basic bool) error {
 	if n.Kind != yaml.MappingNode {
 		return errors.New("expected Mappings to be a MappingNode")
 	}
@@ -200,41 +238,104 @@ func writeMap(sb *strings.Builder, n *yaml.Node, indent string) error {
 	for i := 0; i < len(n.Content); i += 2 {
 		name := n.Content[i].Value
 		val := n.Content[i+1]
-		w(sb, "%s[\"%s\"]", indent, name)
-		if val.Kind == yaml.ScalarNode {
-			sb.WriteString(" = ")
-		} else {
-			sb.WriteString(" {\n")
-		}
-		if val.Kind == yaml.ScalarNode {
-			writeNode(sb, val, "")
+		if basic {
+			w(sb, "%s[\"%s\"]", indent, name)
+			if val.Kind == yaml.ScalarNode {
+				sb.WriteString(" = ")
+				writeNode(sb, val, "", basic)
+			} else {
+				sb.WriteString(" {\n")
+				writeNode(sb, val, indent+"    ", basic)
+				w(sb, "%s}\n", indent)
+			}
 		} else {
-			writeNode(sb, val, indent+"    ")
-			w(sb, "%s}\n", indent)
+			switch name {
+			case SUB:
+				w(sb, "cfn.Sub(\"%s\")\n", val.Value)
+			case REF:
+				w(sb, "cfn.Ref(\"%s\")\n", val.Value)
+			case GETATT:
+				w(sb, "cfn.GetAtt(\"%s\", \"%s\")\n",
+					val.Content[0].Value, val.Content[1].Value)
+			case EQUALS:
+				w(sb, "cfn.Equals(\"%s\", \"%s\")\n",
+					val.Content[0].Value, val.Content[1].Value)
+			case CONTAINS:
+				w(sb, "cfn.Contains(\"%s\", \"%s\")\n",
+					val.Content[0].Value, val.Content[1].Value)
+			case FINDINMAP:
+				w(sb, "cfn.FindInMap(\"%s\", \"%s\", \"%s\")\n",
+					val.Content[0].Value, val.Content[1].Value, val.Content[2].Value)
+			case GETAZS:
+				w(sb, "cfn.GetAZs(\"%s\")\n", val.Value)
+			case SELECT:
+				w(sb, "cfn.Select(\"%s\", \"%s\")\n",
+					val.Content[0].Value, val.Content[1].Value)
+			default:
+				w(sb, "%s%s", indent, name)
+				if val.Kind == yaml.ScalarNode {
+					sb.WriteString(" = ")
+					writeNode(sb, val, "", basic)
+				} else {
+					if isIntrinsic(val.Content[0].Value) {
+						w(sb, " = ")
+						writeNode(sb, val, indent+"        ", basic)
+					} else {
+						w(sb, " {\n")
+						writeNode(sb, val, indent+"        ", basic)
+						sb.WriteString(indent + "    }\n")
+					}
+				}
+			}
 		}
 	}
-	// TODO: Rewrite intrinsic functions to use cfn. helpers
 	return nil
 }
 
-func writeNode(sb *strings.Builder, n *yaml.Node, indent string) error {
+func isNum(s string) bool {
+	_, err := strconv.ParseFloat(s, 64)
+	return err == nil
+}
+
+func fixScalar(s string) string {
+	// This is hard to do correctly without inspecting the schema.
+	// Sometimes booleans and numbers are strings and YAML doesn't care
+	// Also booleans are extra complicated
+
+	boolStrings := []string{
+		"true",
+		"false",
+	}
+
+	if slices.Contains(boolStrings, s) {
+		return s
+	}
+
+	if isNum(s) {
+		return s
+	}
+
+	return fmt.Sprintf("\"%s\"", strings.Replace(s, "\n", "\\n", -1))
+}
+
+func writeNode(sb *strings.Builder, n *yaml.Node, indent string, basic bool) error {
 	switch n.Kind {
 	case yaml.ScalarNode:
-		w(sb, "%s\"%s\"\n", indent, n.Value)
+		w(sb, "%s%s\n", indent, fixScalar(n.Value))
 	case yaml.SequenceNode:
-		return writeSequence(sb, n, indent)
+		return writeSequence(sb, n, indent, basic)
 	case yaml.MappingNode:
-		return writeMap(sb, n, indent)
+		return writeMap(sb, n, indent, basic)
 	}
 	return nil
 }
 
-func addSection(section cft.Section, n *yaml.Node, sb *strings.Builder) error {
+func addSection(section cft.Section, n *yaml.Node, sb *strings.Builder, basic bool) error {
 	switch section {
 	case cft.AWSTemplateFormatVersion:
 		fallthrough
 	case cft.Description:
-		w(sb, "%s = \"%s\"\n", section, n.Value)
+		w(sb, "%s = %s\n", section, fixScalar(n.Value))
 	case cft.Parameters:
 		if n.Kind != yaml.MappingNode {
 			return errors.New("expected Parameters to be a MappingNode")
@@ -250,21 +351,21 @@ func addSection(section cft.Section, n *yaml.Node, sb *strings.Builder) error {
 		}
 		w(sb, "%s {\n", section)
 		for i := 0; i < len(n.Content); i += 2 {
-			writeResource(sb, n.Content[i].Value, n.Content[i+1])
+			writeResource(sb, n.Content[i].Value, n.Content[i+1], basic)
 		}
 		sb.WriteString("}\n")
 	case cft.Mappings:
 		fallthrough
 	case cft.Metadata:
 		w(sb, "%s {\n", section)
-		if err := writeMap(sb, n, "    "); err != nil {
+		if err := writeMap(sb, n, "    ", basic); err != nil {
 			return fmt.Errorf("unable to write %s section: %v", section, err)
 		}
 		sb.WriteString("}\n")
 	case cft.Rules:
 	case cft.Conditions:
 		w(sb, "%s {\n", section)
-		if err := writeMap(sb, n, "    "); err != nil {
+		if err := writeMap(sb, n, "    ", basic); err != nil {
 			return fmt.Errorf("unable to write %s section: %v", section, err)
 		}
 		sb.WriteString("}\n")
@@ -277,7 +378,7 @@ func addSection(section cft.Section, n *yaml.Node, sb *strings.Builder) error {
 
 // CftToPkl serializes the template as pkl.
 // It assumes that the user is import the cloudformation package
-func CftToPkl(t cft.Template) (string, error) {
+func CftToPkl(t cft.Template, basic bool, pklPackageAlias string) (string, error) {
 	if t.Node == nil || len(t.Node.Content) != 1 {
 		return "", errors.New("expected t.Node.Content[0]")
 	}
@@ -286,31 +387,35 @@ func CftToPkl(t cft.Template) (string, error) {
 		return "", errors.New("expected even number of map elements")
 	}
 	var sb strings.Builder
-	w(&sb, "amends \"%s/template.pkl\"\n", PklPackageAlias)
-	w(&sb, "import \"%s/cloudformation.pkl\" as cfn\n", PklPackageAlias)
 
-	// Peek at all resources and import their types
-	resources, err := t.GetSection(cft.Resources)
-	if err != nil {
-		return "", err
-	}
-	imports := make([]string, 0)
-	for i := 0; i < len(resources.Content); i += 2 {
-		resource := resources.Content[i+1]
-		modulePath := getModulePath(resource)
-		if modulePath != "" {
-			if !slices.Contains(imports, modulePath) {
-				imports = append(imports, modulePath)
-				w(&sb, "import \"%s/%s\"\n", PklPackageAlias, modulePath)
+	if !basic {
+		w(&sb, "amends \"%s/template.pkl\"\n", pklPackageAlias)
+		w(&sb, "import \"%s/cloudformation.pkl\" as cfn\n", pklPackageAlias)
+
+		// Peek at all resources and import their types
+		resources, err := t.GetSection(cft.Resources)
+		if err != nil {
+			return "", err
+		}
+		imports := make([]string, 0)
+		for i := 0; i < len(resources.Content); i += 2 {
+			resource := resources.Content[i+1]
+			modulePath := getModulePath(resource)
+			if modulePath != "" {
+				if !slices.Contains(imports, modulePath) {
+					imports = append(imports, modulePath)
+					w(&sb, "import \"%s/%s\"\n", pklPackageAlias, modulePath)
+				}
 			}
 		}
 	}
 
 	// Write each section
 	for i := 0; i < len(m.Content); i += 2 {
+		sb.WriteString("\n")
 		section := m.Content[i].Value
 		val := m.Content[i+1]
-		if err := addSection(cft.Section(section), val, &sb); err != nil {
+		if err := addSection(cft.Section(section), val, &sb, basic); err != nil {
 			return "", fmt.Errorf("failed to add %s: %v", section, err)
 		}
 	}
diff --git a/internal/aws/cfn/cfn.go b/internal/aws/cfn/cfn.go
index e2854cc8..61f29ef7 100644
--- a/internal/aws/cfn/cfn.go
+++ b/internal/aws/cfn/cfn.go
@@ -468,8 +468,14 @@ func CreateChangeSet(template cft.Template, params []types.Parameter, tags map[s
 		input.TemplateBody = ptr.String(templateBody)
 		config.Debugf("About to create changeset with body:\n%s", templateBody)
 		for _, param := range params {
-			config.Debugf("Parameter Key: %s, Value: %s",
-				*param.ParameterKey, *param.ParameterValue)
+			var k, v string
+			if param.ParameterKey != nil {
+				k = *param.ParameterKey
+			}
+			if param.ParameterValue != nil {
+				v = *param.ParameterValue
+			}
+			config.Debugf("Parameter Key: %s, Value: %s", k, v)
 		}
 	}
 
diff --git a/internal/cmd/fmt/fmt.go b/internal/cmd/fmt/fmt.go
index 6746fb69..d6f48864 100644
--- a/internal/cmd/fmt/fmt.go
+++ b/internal/cmd/fmt/fmt.go
@@ -25,6 +25,12 @@ var writeFlag bool
 var unsortedFlag bool
 var dataModel bool
 
+// pklPackageAlias is the package name to use in module imports
+var pklPackageAlias string = "@cfn"
+
+// pklBasic is set by the --pkl-basic CLI arg to emit Pkl without any imports
+var pklBasic bool = false
+
 type result struct {
 	name   string
 	output string
@@ -44,7 +50,7 @@ func formatString(input string, res *result) {
 	if dataModel {
 		res.output = node.ToJson(source.Node)
 	} else if pklFlag {
-		res.output, err = format.CftToPkl(source)
+		res.output, err = format.CftToPkl(source, pklBasic, pklPackageAlias)
 		if err != nil {
 			res.err = err
 			return
@@ -195,11 +201,12 @@ var Cmd = &cobra.Command{
 func init() {
 	Cmd.Flags().BoolVarP(&jsonFlag, "json", "j", false, "Output the template as JSON (default format: YAML).")
 	Cmd.Flags().BoolVarP(&pklFlag, "pkl", "p", false, "Output the template as Pkl (default format: YAML).")
+	Cmd.Flags().BoolVar(&pklBasic, "pkl-basic", false, "Don't use Pkl modules for output")
 	Cmd.Flags().BoolVarP(&verifyFlag, "verify", "v", false, "Check if the input is already correctly formatted and exit.\nThe exit status will be 0 if so and 1 if not.")
 	Cmd.Flags().BoolVarP(&writeFlag, "write", "w", false, "Write the output back to the file rather than to stdout.")
 	Cmd.Flags().BoolVarP(&unsortedFlag, "unsorted", "u", false, "Do not sort the template's properties.")
 	Cmd.Flags().BoolVar(&config.Debug, "debug", false, "Output debugging information")
 	Cmd.Flags().BoolVar(&dataModel, "datamodel", false, "Output the go yaml data model")
-	Cmd.Flags().StringVar(&format.PklPackageAlias, "pkl-package", "@cfn", "An alias or full package URI for the Pkl package for generated Pkl files")
+	Cmd.Flags().StringVar(&pklPackageAlias, "pkl-package", "@cfn", "An alias or full package URI for the Pkl package for generated Pkl files")
 	Cmd.Flags().StringVar(&format.NodeStyle, "node-style", "", format.NodeStyleDocs)
 }
diff --git a/internal/config/version.go b/internal/config/version.go
index 096e1024..074b3039 100644
--- a/internal/config/version.go
+++ b/internal/config/version.go
@@ -5,5 +5,5 @@ const (
 	NAME = "Rain"
 
 	// VERSION is the application's version string
-	VERSION = "v1.8.3"
+	VERSION = "v1.8.4"
 )
diff --git a/pkl/cloudformation.pkl b/pkl/cloudformation.pkl
index eb0b5ef2..b221de3c 100644
--- a/pkl/cloudformation.pkl
+++ b/pkl/cloudformation.pkl
@@ -18,7 +18,7 @@ open class Resource {
     CreationPolicy: Mapping? 
     DeletionPolicy: String?
     DependsOn: Listing<String>?
-    Metadata: Mapping?
+    Metadata: Dynamic?
     UpdatePolicy: Mapping?
     UpdateReplacePolicy: String?
     Condition: String?
diff --git a/pkl/template.pkl b/pkl/template.pkl
index 933450d3..2a0a4c5f 100644
--- a/pkl/template.pkl
+++ b/pkl/template.pkl
@@ -6,7 +6,7 @@ import "cloudformation.pkl"
 
 Description: String?
 AWSTemplateFormatVersion: String = "2010-09-09"
-Metadata: Mapping?
+Metadata: Dynamic?
 Parameters: Mapping<String, cloudformation.Parameter>?
 Resources: Mapping<String, cloudformation.Resource> 
 Rules: Mapping<String, cloudformation.Rule>?
diff --git a/scripts/yaml2pkl-roundtrip.sh b/scripts/yaml2pkl-roundtrip.sh
new file mode 100755
index 00000000..0fc562b6
--- /dev/null
+++ b/scripts/yaml2pkl-roundtrip.sh
@@ -0,0 +1,46 @@
+#!/usr/local/bin/bash
+#
+# $1 is the name of the yaml file
+# If $2 is "modules", use modules instead of basic syntax
+#
+
+set -euo pipefail
+
+mkdir -p /tmp/pkl
+
+# Link Pkl modules so we can evaluate high level conversions
+ln -F -s -v -h $(realpath pkl) /tmp/pkl/modules
+
+name=$(basename ${1} | sed s/\.yaml/\.pkl/g)
+pklOutput="/tmp/pkl/${name}"
+yamlOutput="/tmp/pkl/$(basename ${1})"
+
+# Default to basic
+pklBasic="--pkl-basic"
+
+if [ $# -eq 2 ]
+then
+    if [ "$2" == "modules" ]
+    then
+        pklBasic="--pkl-package modules"
+    fi
+fi
+
+echo "Converting to Pkl"
+echo ""
+./rain fmt --pkl ${pklBasic} "$1" > "${pklOutput}"
+cat "${pklOutput}"
+echo ""
+
+echo "Converting back to YAML"
+echo ""
+pkl eval "${pklOutput}" -f yaml | ./rain fmt > "${yamlOutput}"
+cat "${yamlOutput}"
+echo ""
+
+echo "Linting"
+cfn-lint "${yamlOutput}"
+
+echo "Success!"
+
+


Test patch:
diff --git a/cft/format/pkl_test.go b/cft/format/pkl_test.go
new file mode 100644
index 00000000..52c2fcd7
--- /dev/null
+++ b/cft/format/pkl_test.go
@@ -0,0 +1,103 @@
+package format
+
+import (
+	"testing"
+
+	"github.com/aws-cloudformation/rain/cft/parse"
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestYamlToPkl(t *testing.T) {
+	input := `
+Parameters:
+  Name:
+    Type: String
+Resources:
+  MyBucket:
+    Type: AWS::S3::Bucket
+    Properties:
+        BucketName: 
+          Ref: Name
+`
+
+	expected := `amends "@cfn/template.pkl"
+import "@cfn/cloudformation.pkl" as cfn
+import "@cfn/aws/s3/bucket.pkl"
+
+Parameters {
+    ["Name"] {
+        Type = "String"
+    }
+}
+
+Resources {
+    ["MyBucket"] = new bucket.Bucket {
+        Type = "AWS::S3::Bucket"
+        BucketName = cfn.Ref("Name")
+
+    }
+
+}
+`
+
+	template, err := parse.String(input)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	actual, err := CftToPkl(template, false, "@cfn")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if d := cmp.Diff(expected, actual); d != "" {
+		t.Errorf(d)
+	}
+}
+
+func TestYamlToPklBasic(t *testing.T) {
+	input := `
+Parameters:
+  Name:
+    Type: String
+Resources:
+  MyBucket:
+    Type: AWS::S3::Bucket
+    Properties:
+        BucketName: 
+          Ref: Name
+`
+
+	expected := `
+Parameters {
+    ["Name"] {
+        Type = "String"
+    }
+}
+
+Resources {
+    ["MyBucket"] {
+        Type = "AWS::S3::Bucket"
+        Properties {
+            ["BucketName"] {
+                ["Ref"] = "Name"
+            }
+        }
+    }
+
+}
+`
+	template, err := parse.String(input)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	actual, err := CftToPkl(template, true, "@cfn")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if d := cmp.Diff(expected, actual); d != "" {
+		t.Errorf(d)
+	}
+}
diff --git a/test/pkl/condition-basic.pkl b/test/pkl/condition-basic.pkl
new file mode 100644
index 00000000..aa467884
--- /dev/null
+++ b/test/pkl/condition-basic.pkl
@@ -0,0 +1,108 @@
+
+AWSTemplateFormatVersion = "2010-09-09"
+
+Parameters {
+    ["EnvType"] {
+        Description = "Environment type."
+        Default = "test"
+        Type = "String"
+        AllowedValues {
+            "prod"
+            "test"
+        }
+    }
+}
+
+Conditions {
+    ["CreateProdResources"] {
+        ["Fn::Equals"] {
+             new {
+                ["Ref"] = "EnvType"
+            }
+            "prod"
+        }
+    }
+}
+
+Mappings {
+    ["Map1"] {
+        ["Key1"] {
+            ["Name"] = "Val1.1"
+        }
+        ["Key2"] {
+            ["Name"] = "Val1.2"
+        }
+    }
+    ["Map2"] {
+        ["Key1"] {
+            ["Name"] = "Val2.1"
+        }
+        ["Key2"] {
+            ["Name"] = "Val2.2"
+        }
+    }
+}
+
+Resources {
+    ["EC2Instance"] {
+        Type = "AWS::EC2::Instance"
+        Properties {
+            ["ImageId"] = "ami-0ff8a91507f77f867"
+        }
+    }
+
+    ["MountPoint"] {
+        Type = "AWS::EC2::VolumeAttachment"
+        Condition = "CreateProdResources"
+        Properties {
+            ["InstanceId"] {
+                ["Ref"] = "EC2Instance"
+            }
+            ["VolumeId"] {
+                ["Ref"] = "NewVolume"
+            }
+            ["Device"] = "/dev/sdh"
+        }
+    }
+
+    ["NewVolume"] {
+        Type = "AWS::EC2::Volume"
+        Condition = "CreateProdResources"
+        Properties {
+            ["Size"] = "100"
+            ["AvailabilityZone"] {
+                ["Fn::GetAtt"] {
+                    "EC2Instance"
+                    "AvailabilityZone"
+                }
+            }
+        }
+    }
+
+    ["Bucket1"] {
+        Type = "AWS::S3::Bucket"
+        Properties {
+            ["BucketName"] {
+                ["Fn::FindInMap"] {
+                    "Map1"
+                    "Key1"
+                    "Name"
+                }
+            }
+        }
+    }
+
+    ["Bucket2"] {
+        Type = "AWS::S3::Bucket"
+        Properties {
+            ["BucketName"] {
+                ["Fn::FindInMap"] {
+                    "Map2"
+                    "Key1"
+                    "Name"
+                }
+            }
+        }
+    }
+
+}
diff --git a/test/templates/bucket.yaml b/test/templates/bucket.yaml
new file mode 100644
index 00000000..ef790037
--- /dev/null
+++ b/test/templates/bucket.yaml
@@ -0,0 +1,14 @@
+Parameters:
+  AppName:
+    Description: This name be used as a prefix for resource names
+    Type: String
+
+Resources:
+  ObjectStorageBucket:
+    Type: AWS::S3::Bucket
+    Properties:
+      BucketEncryption:
+        ServerSideEncryptionConfiguration:
+          - ServerSideEncryptionByDefault:
+              SSEAlgorithm: AES256
+      BucketName: !Sub ${AppName}-${AWS::Region}-${AWS::AccountId}
diff --git a/test/templates/reuse-param.yaml b/test/templates/reuse-param.yaml
new file mode 100644
index 00000000..9ab04d75
--- /dev/null
+++ b/test/templates/reuse-param.yaml
@@ -0,0 +1,20 @@
+Parameters:
+  LogName: 
+    Type: String
+  Query:
+    Type: String
+    Default: "fields @message"
+Resources:
+  Log:
+    Type: AWS::Logs::LogGroup
+    Properties:
+      LogGroupName: !Ref LogName
+  Query1:
+    Type: AWS::Logs::QueryDefinition
+    Properties:
+      Name: Query1
+      QueryString: !Ref Query 
+      LogGroupNames:
+      - !Ref LogName
+
+