Repository: aws-cloudformation/rain
Pull request #626 (2025-01-08 23:29:57)

Added uncdk to fmt command
`rain fmt --uncdk` removes CDK Metadata and simplifies logical IDs from templates that were generated with `cdk synth`. This can be useful for pipelines where CDK is used strictly as a code generator and the templates are deployed separately, or if you use CDK as a starting point for declarative YAML CloudFormation development. 

This command only looks at a single template. The `rain merge` command can be used to stitch together separate templates from CDK output.


Patch:
diff --git a/cft/cft.go b/cft/cft.go
index 0698f1ef..fa5ec51f 100644
--- a/cft/cft.go
+++ b/cft/cft.go
@@ -97,7 +97,6 @@ func (t Template) GetParameter(name string) (*yaml.Node, error) {
 func (t Template) GetNode(section Section, name string) (*yaml.Node, error) {
 	_, resMap, _ := s11n.GetMapValue(t.Node.Content[0], string(section))
 	if resMap == nil {
-		config.Debugf("GetNode t.Node: %s", node.ToSJson(t.Node))
 		return nil, fmt.Errorf("unable to locate the %s node", section)
 	}
 	// TODO: Some Sections are not Maps
@@ -144,7 +143,6 @@ func (t Template) GetSection(section Section) (*yaml.Node, error) {
 	m := t.Node.Content[0]
 	_, s, _ := s11n.GetMapValue(m, string(section))
 	if s == nil {
-		config.Debugf("GetSection t.Node: %s", node.ToSJson(t.Node))
 		return nil, fmt.Errorf("unable to locate the %s node", section)
 	}
 	return s, nil
@@ -203,3 +201,26 @@ func (t Template) GetResourcesOfType(typeName string) []*Resource {
 	}
 	return retval
 }
+
+// RemoveEmptySections removes sections from the template that have no content
+func (t Template) RemoveEmptySections() {
+	if t.Node == nil {
+		config.Debugf("t.Node is nil")
+		return
+	}
+	m := t.Node.Content[0]
+	sectionsToRemove := make([]string, 0)
+	for i := 0; i < len(m.Content); i++ {
+		if i%2 != 0 {
+			continue
+		}
+		name := m.Content[i].Value
+		node := m.Content[i+1]
+		if len(node.Content) == 0 {
+			sectionsToRemove = append(sectionsToRemove, name)
+		}
+	}
+	for _, name := range sectionsToRemove {
+		node.RemoveFromMap(m, name)
+	}
+}
diff --git a/cft/format/json.go b/cft/format/json.go
index b69b5d93..da9d2039 100644
--- a/cft/format/json.go
+++ b/cft/format/json.go
@@ -3,11 +3,9 @@ package format
 import (
 	"bytes"
 	"encoding/json"
-	"fmt"
 	"strings"
 
 	"github.com/aws-cloudformation/rain/cft/parse"
-	"github.com/aws-cloudformation/rain/internal/config"
 	"gopkg.in/yaml.v3"
 )
 
@@ -34,8 +32,6 @@ func handleScalar(node *yaml.Node) interface{} {
 		panic(err)
 	}
 
-	config.Debugf("intermediate: %v", string(intermediate))
-
 	var out interface{}
 	err = yaml.Unmarshal(intermediate, &out)
 	if err != nil {
@@ -72,8 +68,6 @@ func Jsonise(node *yaml.Node) interface{} {
 
 func convertToJSON(in string) string {
 
-	config.Debugf("convertToJson: %v", in)
-
 	var d yaml.Node
 
 	err := yaml.Unmarshal([]byte(in), &d)
@@ -104,14 +98,11 @@ func PrettyPrint(i interface{}) string {
 
 // ToJson overrides the default behavior of json.Marshal to leave < > alone
 func ToJson(i interface{}, indent string) ([]byte, error) {
-	si := fmt.Sprintf("%#v", i)
-	config.Debugf("ToJson(%s)", si)
 	buf := &bytes.Buffer{}
 	enc := json.NewEncoder(buf)
 	enc.SetEscapeHTML(false)
 	enc.SetIndent("", indent)
 	err := enc.Encode(i)
 	retval := bytes.TrimRight(buf.Bytes(), "\n")
-	config.Debugf("ToJson retval: %s", retval)
 	return retval, err
 }
diff --git a/cft/format/uncdk.go b/cft/format/uncdk.go
new file mode 100644
index 00000000..55caeee0
--- /dev/null
+++ b/cft/format/uncdk.go
@@ -0,0 +1,294 @@
+package format
+
+import (
+	"fmt"
+	"strings"
+	"unicode"
+
+	"github.com/aws-cloudformation/rain/cft"
+	"github.com/aws-cloudformation/rain/cft/visitor"
+	"github.com/aws-cloudformation/rain/internal/node"
+	"github.com/aws-cloudformation/rain/internal/s11n"
+	"gopkg.in/yaml.v3"
+)
+
+func UnCDK(t cft.Template) error {
+
+	// Remove these nodes:
+	//
+	// Resources:
+	//   CDKMetadata:
+	//   {*}:
+	//     Metadata:
+	//       aws:cdk:path:
+	//       aws:asset:path:
+	//       aws:asset:property:
+	// Conditions:
+	//   CDKMetadataAvailable:
+	// Parameters:
+	//   BootstrapVersion:
+	// Rules:
+	//   CheckBootstrapVersion:
+
+	removals := make(map[string][]string)
+	removals[string(cft.Resources)] = []string{"CDKMetadata"}
+	removals[string(cft.Conditions)] = []string{"CDKMetadataAvailable"}
+	removals[string(cft.Parameters)] = []string{"BootstrapVersion"}
+	removals[string(cft.Rules)] = []string{"CheckBootstrapVersion"}
+
+	for k, v := range removals {
+		section, err := t.GetSection(cft.Section(k))
+		if err != nil {
+			continue // Section not found
+		}
+		for _, name := range v {
+			n := s11n.GetMap(section, name)
+			if n != nil {
+				node.RemoveFromMap(section, name)
+			}
+		}
+	}
+
+	// Iterate through all the resources to remove cdk metadata,
+	// And fix the logical ids so they are easier to read
+
+	resources, err := t.GetSection(cft.Resources)
+	if err != nil {
+		return err
+	}
+
+	commonPrefix := getCommonResourcePrefix(t)
+
+	// Store the resource logical id node each time we see a repeated name
+	// Start without a number, for example "Bucket"
+	// If we see another one, fix the first one to be "Bucket0"
+	allNames := make(map[string][]*yaml.Node)
+
+	for i := 0; i < len(resources.Content); i += 1 {
+		if i%2 != 0 {
+			continue
+		}
+		logicalId := resources.Content[i].Value
+		resource := resources.Content[i+1]
+
+		// Simplify the logical id
+		_, typ, _ := s11n.GetMapValue(resource, "Type")
+		if typ == nil {
+			return fmt.Errorf("expected %s to have Type", logicalId)
+		}
+		oldName := resources.Content[i].Value
+		newName := createNewName(typ.Value, logicalId, commonPrefix)
+		if nameNodes, ok := allNames[newName]; ok {
+			// We've seen this one before
+			nameNodes = append(nameNodes, resources.Content[i])
+			allNames[newName] = nameNodes
+			for nodeIdx, node := range nameNodes {
+				sequential := fmt.Sprintf("%s%d", newName, nodeIdx)
+				priorValue := node.Value
+				node.Value = sequential
+				replaceNames(t, priorValue, sequential)
+			}
+		} else {
+			// We haven't seen this name yet
+			resources.Content[i].Value = newName
+			allNames[newName] = make([]*yaml.Node, 0)
+			allNames[newName] = append(allNames[newName], resources.Content[i])
+			replaceNames(t, oldName, newName)
+		}
+
+		// Remove the cdk path and asset metadata
+		_, metadata, _ := s11n.GetMapValue(resource, string(cft.Metadata))
+		if metadata != nil {
+			stringsToRemove := []string{
+				"aws:cdk:path",
+				"aws:asset:path",
+				"aws:asset:property",
+				"aws:asset:is-bundled",
+				"cfn_nag",
+			}
+			for _, s := range stringsToRemove {
+				node.RemoveFromMap(metadata, s)
+			}
+			// If the resource Metadata node is empty, remove it
+			if len(metadata.Content) == 0 {
+				node.RemoveFromMap(resource, string(cft.Metadata))
+			}
+		}
+	}
+
+	// Remove any empty sections
+	t.RemoveEmptySections()
+
+	// Replace Joins with Subs to make them easier to read
+	joinToSub(t)
+
+	return nil // TODO
+
+}
+
+func joinSeqToString(seq *yaml.Node) string {
+	if len(seq.Content) != 2 {
+		return "Invalid Join"
+	}
+	j := seq.Content[0].Value
+	tokens := seq.Content[1]
+	retval := ""
+	for i, token := range tokens.Content {
+		if i != 0 {
+			retval += j
+		}
+		if token.Kind == yaml.ScalarNode {
+			retval += token.Value
+		}
+		if token.Kind == yaml.MappingNode {
+			if token.Content[0].Value == "Ref" {
+				retval += "${" + token.Content[1].Value + "}"
+			}
+			if token.Content[0].Value == "Fn::GetAtt" {
+				retval += "${" + token.Content[1].Content[0].Value
+				retval += "." + token.Content[1].Content[1].Value + "}"
+			}
+		}
+	}
+	return retval
+}
+
+func joinToSub(t cft.Template) {
+	vf := func(n *visitor.Visitor) {
+		yamlNode := n.GetYamlNode()
+		if yamlNode.Kind == yaml.MappingNode {
+			if len(yamlNode.Content) == 2 && yamlNode.Content[0].Value == "Fn::Join" {
+				seq := yamlNode.Content[1]
+				if seq.Kind == yaml.SequenceNode {
+					yamlNode.Content[0].Value = "Fn::Sub"
+					yamlNode.Content[1].Value = joinSeqToString(seq)
+					yamlNode.Content[1].Kind = yaml.ScalarNode
+					yamlNode.Content[1].Content = make([]*yaml.Node, 0)
+				}
+			}
+		}
+	}
+	visitor := visitor.NewVisitor(t.Node)
+	visitor.Visit(vf)
+
+}
+
+func replaceNames(t cft.Template, oldName, newName string) {
+	vf := func(n *visitor.Visitor) {
+		yamlNode := n.GetYamlNode()
+		if yamlNode.Kind == yaml.ScalarNode {
+			if yamlNode.Value == oldName {
+				yamlNode.Value = newName
+			}
+		}
+	}
+	visitor := visitor.NewVisitor(t.Node)
+	visitor.Visit(vf)
+}
+
+// getCommonTemplatePrefix attempts to find a common string that begins all resource names.
+func getCommonResourcePrefix(t cft.Template) string {
+	resources, err := t.GetSection(cft.Resources)
+	if err != nil {
+		return ""
+	}
+	logicalIds := make([]string, 0)
+	for i := 0; i < len(resources.Content); i += 2 {
+		logicalId := resources.Content[i].Value
+		logicalIds = append(logicalIds, logicalId)
+	}
+	return getCommonPrefix(logicalIds)
+}
+
+// getCommonPrefix attempts to find a common string that begins all strings in the slice.
+func getCommonPrefix(logicalIds []string) string {
+	if len(logicalIds) < 2 {
+		return ""
+	}
+	retval := ""
+	prefixes := make([]string, 0)
+	for j := 1; j < len(logicalIds); j++ {
+		prefix := ""
+		for i, c := range logicalIds[0] {
+			second := []rune(logicalIds[j])
+			if len(second) > i && second[i] == c {
+				prefix += string(c)
+			} else {
+				prefixes = append(prefixes, prefix)
+				if retval == "" {
+					retval = prefix
+				}
+				for _, p := range prefixes {
+					// Pick the shortest prefix
+					if len(p) < len(retval) && retval != "" {
+						retval = p
+					}
+				}
+				break
+			}
+		}
+	}
+
+	common := true
+	for _, id := range logicalIds {
+		if !strings.HasPrefix(id, retval) {
+			common = false
+			break
+		}
+	}
+	if common {
+		return retval
+	}
+	return ""
+}
+
+// stripSuffix attempts to remove the random 8 characters at the end of ids
+func stripSuffix(s string) string {
+
+	if len(s) <= 8 {
+		return s
+	}
+
+	// Too simple. For imported constructs, you can end up with several
+	// Strip off the random 8 digit string at the end
+	//return newName[:len(newName)-8]
+
+	suffixLen := 0
+
+	for i := len(s) - 1; i >= 0; i-- {
+		isUpper := unicode.IsUpper(rune(s[i])) && unicode.IsLetter(rune(s[i]))
+		isDigit := unicode.IsDigit(rune(s[i]))
+		if isUpper || isDigit {
+			suffixLen += 1
+		} else {
+			break
+		}
+	}
+
+	if suffixLen == len(s) {
+		return s
+	}
+
+	// Round to the nearest 8 in case a name ended with a capital letter or number
+	suffixLen = suffixLen - (suffixLen % 8)
+
+	return s[:len(s)-suffixLen]
+}
+
+// createNewName converts the cdk generated name into something that is easier to read.
+func createNewName(typeName string, logicalId string, commonPrefix string) string {
+	newName := ""
+	if commonPrefix != "" {
+		newName = strings.Replace(logicalId, commonPrefix, "", -1)
+		return stripSuffix(newName)
+	}
+	tokens := strings.Split(typeName, "::")
+	if len(tokens) == 3 {
+		newName = tokens[2]
+	} else if len(tokens) == 2 && tokens[0] == "Custom" {
+		newName = strings.Replace(tokens[1], "-", "", -1)
+	} else {
+		newName = strings.Replace(typeName, "::", "", -1)
+	}
+	return newName
+}
diff --git a/internal/cmd/fmt/fmt.go b/internal/cmd/fmt/fmt.go
index 5c736c23..0154e445 100644
--- a/internal/cmd/fmt/fmt.go
+++ b/internal/cmd/fmt/fmt.go
@@ -24,6 +24,7 @@ var verifyFlag bool
 var writeFlag bool
 var unsortedFlag bool
 var dataModel bool
+var uncdk bool
 
 // pklPackageAlias is the package name to use in module imports
 var pklPackageAlias string = "@cfn"
@@ -49,6 +50,16 @@ func formatString(input string, res *result) {
 
 	config.Debugf("%s", node.ToSJson(source.Node))
 
+	if uncdk {
+		// Remove CDK Metadata
+		// Simplify logical IDs
+		err := format.UnCDK(source)
+		if err != nil {
+			res.err = ui.Errorf(err, "uncdk failed")
+			return
+		}
+	}
+
 	if dataModel {
 		res.output = node.ToJson(source.Node)
 	} else if pklFlag {
@@ -202,4 +213,5 @@ func init() {
 	Cmd.Flags().BoolVar(&dataModel, "datamodel", false, "Output the go yaml data model")
 	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)
+	Cmd.Flags().BoolVar(&uncdk, "uncdk", false, "Remove CDK Metadata and simplify logical ids")
 }


Test patch:
diff --git a/cft/format/uncdk_test.go b/cft/format/uncdk_test.go
new file mode 100644
index 00000000..d08239c7
--- /dev/null
+++ b/cft/format/uncdk_test.go
@@ -0,0 +1,144 @@
+package format
+
+import (
+	"testing"
+
+	"github.com/aws-cloudformation/rain/cft"
+	"github.com/aws-cloudformation/rain/cft/diff"
+	"github.com/aws-cloudformation/rain/cft/parse"
+	"github.com/aws-cloudformation/rain/internal/config"
+	"gopkg.in/yaml.v3"
+)
+
+func TestRemoveEmptySections(t *testing.T) {
+	src := `
+Parameters: {}
+Resources:
+  Bucket:
+    Type: AWS::S3::Bucket
+`
+	template, err := parse.String(src)
+	if err != nil {
+		t.Fatal(err)
+	}
+	template.RemoveEmptySections()
+
+	params, err := template.GetSection(cft.Parameters)
+	if err == nil && params != nil {
+		t.Fatal("expected Parameters section to be removed")
+	}
+
+}
+
+func TestUnCDK(t *testing.T) {
+
+	path := "../../test/templates/uncdk.yaml"
+	template, err := parse.File(path)
+	if err != nil {
+		t.Fatalf("could not parse %s: %v", path, err)
+	}
+
+	expectPath := "../../test/templates/uncdk-expect.yaml"
+	expectedTemplate, err := parse.File(expectPath)
+	if err != nil {
+		t.Fatalf("could not parse %s: %v", expectPath, err)
+	}
+
+	err = UnCDK(template)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	d := diff.New(template, expectedTemplate)
+	if d.Mode() != "=" {
+		t.Errorf("Output does not match expected: %v", d.Format(true))
+	}
+
+}
+
+func TestGetCommonPrefix(t *testing.T) {
+	testCases := []struct {
+		name           string
+		logicalIds     []string
+		expectedPrefix string
+	}{
+		{
+			name:           "Empty slice",
+			logicalIds:     []string{},
+			expectedPrefix: "",
+		},
+		{
+			name:           "Single element",
+			logicalIds:     []string{"Busket"},
+			expectedPrefix: "",
+		},
+		{
+			name:           "Common prefix",
+			logicalIds:     []string{"BucketA", "BucketB", "BucketC"},
+			expectedPrefix: "Bucket",
+		},
+		{
+			name:           "No common prefix",
+			logicalIds:     []string{"BucketA", "QueueB", "TableC"},
+			expectedPrefix: "",
+		},
+		{
+			name:           "Mixed case",
+			logicalIds:     []string{"BucketA", "bucketB", "BucketC"},
+			expectedPrefix: "",
+		},
+		{
+			name: "TryConvertCdk",
+			logicalIds: []string{
+				"TryConvertCdkQueueA6B3948A",
+				"TryConvertCdkQueueA6B3948B",
+				"TryConvertCdkQueueA6B3948C",
+				"TryConvertCdkQueuePolicy8C365983",
+				"TryConvertCdkQueueTryConvertCdkStackTryConvertCdkTopic4C9C531F7A91899A",
+				"TryConvertCdkTopic2CABFDF4",
+				"TryConvertCdkACustomResource",
+				"TryConvertCdkAnotherCustomResource",
+				"TryConvertCdkAnotherCustomResource2",
+			},
+			expectedPrefix: "TryConvertCdk",
+		},
+	}
+
+	config.Debug = true
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prefix := getCommonPrefix(tc.logicalIds)
+			if prefix != tc.expectedPrefix {
+				t.Errorf("Expected prefix '%s', got '%s'", tc.expectedPrefix, prefix)
+			}
+		})
+	}
+}
+
+func TestJoinSeqToString(t *testing.T) {
+	src := `
+Fn::Join:
+  - ""
+  - - "arn:"
+    - Ref: AWS::Partition
+    - ":logs:"
+    - Ref: AWS::Region
+    - ":"
+    - Ref: AWS::AccountId
+    - ":"
+    - Fn::GetAtt:
+      - "Foo"
+      - "Bar"
+`
+	var n yaml.Node
+	err := yaml.Unmarshal([]byte(src), &n)
+	if err != nil {
+		t.Fatal(err)
+	}
+	sub := joinSeqToString(n.Content[0].Content[1])
+	expect := "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:${Foo.Bar}"
+	if sub != expect {
+		t.Fatalf("unexpected sub: %s", sub)
+	}
+}
diff --git a/test/templates/notcontains.yaml b/test/templates/notcontains.yaml
new file mode 100644
index 00000000..8abb70f5
--- /dev/null
+++ b/test/templates/notcontains.yaml
@@ -0,0 +1,12 @@
+Rules:
+  CheckBootstrapVersion:
+    Assertions:
+      - Assert: !Not
+          - !Contains
+            - - '1'
+              - '2'
+              - '3'
+              - '4'
+              - '5'
+            - !Ref BootstrapVersion
+
diff --git a/test/templates/uncdk-expect.yaml b/test/templates/uncdk-expect.yaml
new file mode 100644
index 00000000..461ae959
--- /dev/null
+++ b/test/templates/uncdk-expect.yaml
@@ -0,0 +1,74 @@
+Resources:
+  Queue0:
+    DeletionPolicy: Delete
+    UpdateReplacePolicy: Delete
+    Type: AWS::SQS::Queue
+    Properties:
+      VisibilityTimeout: 300
+
+  Queue1:
+    DeletionPolicy: Delete
+    UpdateReplacePolicy: Delete
+    Type: AWS::SQS::Queue
+    Properties:
+      VisibilityTimeout: 300
+
+  Queue2:
+    DeletionPolicy: Delete
+    UpdateReplacePolicy: Delete
+    Type: AWS::SQS::Queue
+    Properties:
+      VisibilityTimeout: 300
+
+  QueuePolicy:
+    Type: AWS::SQS::QueuePolicy
+    Metadata:
+      Comment: A comment
+    Properties:
+      PolicyDocument:
+        Statement:
+          - Action: sqs:SendMessage
+            Condition:
+              ArnEquals:
+                aws:SourceArn: !Ref Topic
+            Effect: Allow
+            Principal:
+              Service: sns.amazonaws.com
+            Resource: !GetAtt Queue0.Arn
+        Version: "2012-10-17"
+      Queues:
+        - !Ref Queue0
+        - !Ref Queue1
+
+  QueueStackTopic:
+    Type: AWS::SNS::Subscription
+    DependsOn:
+      - QueuePolicy
+    Properties:
+      Endpoint: !GetAtt Queue0.Arn
+      Protocol: sqs
+      TopicArn: !Ref Topic
+
+  Topic:
+    Type: AWS::SNS::Topic
+
+  ACustomResource:
+    Type: Custom::CustomThing
+    Properties:
+      ServiceToken: foo
+
+  AnotherCustomResource:
+    Type: Custom::AnotherCustomThing
+    Properties:
+      ServiceToken: foo
+
+  AnotherCustomResource2:
+    Type: Custom::AnotherCustomThing
+    Properties:
+      ServiceToken: foo
+      FixThisJoin:
+        Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:${Foo.Bar}
+
+Outputs:
+  TryConvertCdkQueueA6B3948CName:
+    Value: !Ref Queue2
diff --git a/test/templates/uncdk.yaml b/test/templates/uncdk.yaml
new file mode 100644
index 00000000..d79b4da2
--- /dev/null
+++ b/test/templates/uncdk.yaml
@@ -0,0 +1,220 @@
+Parameters:
+  BootstrapVersion:
+    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]
+    Type: AWS::SSM::Parameter::Value<String>
+    Default: /cdk-bootstrap/hnb659fds/version
+
+Rules:
+  CheckBootstrapVersion:
+    Assertions:
+      - Assert: !Not
+          - Fn::Contains:
+              - - "1"
+                - "2"
+                - "3"
+                - "4"
+                - "5"
+              - !Ref BootstrapVersion
+        AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.
+
+Conditions:
+  CDKMetadataAvailable: !Or
+    - !Or
+      - !Equals
+        - !Ref AWS::Region
+        - af-south-1
+      - !Equals
+        - !Ref AWS::Region
+        - ap-east-1
+      - !Equals
+        - !Ref AWS::Region
+        - ap-northeast-1
+      - !Equals
+        - !Ref AWS::Region
+        - ap-northeast-2
+      - !Equals
+        - !Ref AWS::Region
+        - ap-northeast-3
+      - !Equals
+        - !Ref AWS::Region
+        - ap-south-1
+      - !Equals
+        - !Ref AWS::Region
+        - ap-south-2
+      - !Equals
+        - !Ref AWS::Region
+        - ap-southeast-1
+      - !Equals
+        - !Ref AWS::Region
+        - ap-southeast-2
+      - !Equals
+        - !Ref AWS::Region
+        - ap-southeast-3
+    - !Or
+      - !Equals
+        - !Ref AWS::Region
+        - ap-southeast-4
+      - !Equals
+        - !Ref AWS::Region
+        - ca-central-1
+      - !Equals
+        - !Ref AWS::Region
+        - ca-west-1
+      - !Equals
+        - !Ref AWS::Region
+        - cn-north-1
+      - !Equals
+        - !Ref AWS::Region
+        - cn-northwest-1
+      - !Equals
+        - !Ref AWS::Region
+        - eu-central-1
+      - !Equals
+        - !Ref AWS::Region
+        - eu-central-2
+      - !Equals
+        - !Ref AWS::Region
+        - eu-north-1
+      - !Equals
+        - !Ref AWS::Region
+        - eu-south-1
+      - !Equals
+        - !Ref AWS::Region
+        - eu-south-2
+    - !Or
+      - !Equals
+        - !Ref AWS::Region
+        - eu-west-1
+      - !Equals
+        - !Ref AWS::Region
+        - eu-west-2
+      - !Equals
+        - !Ref AWS::Region
+        - eu-west-3
+      - !Equals
+        - !Ref AWS::Region
+        - il-central-1
+      - !Equals
+        - !Ref AWS::Region
+        - me-central-1
+      - !Equals
+        - !Ref AWS::Region
+        - me-south-1
+      - !Equals
+        - !Ref AWS::Region
+        - sa-east-1
+      - !Equals
+        - !Ref AWS::Region
+        - us-east-1
+      - !Equals
+        - !Ref AWS::Region
+        - us-east-2
+      - !Equals
+        - !Ref AWS::Region
+        - us-west-1
+    - !Equals
+      - !Ref AWS::Region
+      - us-west-2
+
+Resources:
+  TryConvertCdkQueueA6B3948A:
+    DeletionPolicy: Delete
+    UpdateReplacePolicy: Delete
+    Type: AWS::SQS::Queue
+    Metadata:
+      aws:cdk:path: TryConvertCdkStack/TryConvertCdkQueue/Resource
+    Properties:
+      VisibilityTimeout: 300
+  TryConvertCdkQueueA6B3948B:
+    DeletionPolicy: Delete
+    UpdateReplacePolicy: Delete
+    Type: AWS::SQS::Queue
+    Metadata:
+      aws:cdk:path: TryConvertCdkStack/TryConvertCdkQueue/Resource
+    Properties:
+      VisibilityTimeout: 300
+  TryConvertCdkQueueA6B3948C:
+    DeletionPolicy: Delete
+    UpdateReplacePolicy: Delete
+    Type: AWS::SQS::Queue
+    Metadata:
+      aws:cdk:path: TryConvertCdkStack/TryConvertCdkQueue/Resource
+    Properties:
+      VisibilityTimeout: 300
+
+  TryConvertCdkQueuePolicy8C365983:
+    Type: AWS::SQS::QueuePolicy
+    Metadata:
+      aws:cdk:path: TryConvertCdkStack/TryConvertCdkQueue/Policy/Resource
+      Comment: A comment
+    Properties:
+      PolicyDocument:
+        Statement:
+          - Action: sqs:SendMessage
+            Condition:
+              ArnEquals:
+                aws:SourceArn: !Ref TryConvertCdkTopic2CABFDF4
+            Effect: Allow
+            Principal:
+              Service: sns.amazonaws.com
+            Resource: !GetAtt TryConvertCdkQueueA6B3948A.Arn
+        Version: "2012-10-17"
+      Queues:
+        - !Ref TryConvertCdkQueueA6B3948A
+        - !Ref TryConvertCdkQueueA6B3948B
+
+  TryConvertCdkQueueTryConvertCdkStackTryConvertCdkTopic4C9C531F7A91899A:
+    Type: AWS::SNS::Subscription
+    DependsOn:
+      - TryConvertCdkQueuePolicy8C365983
+    Metadata:
+      aws:cdk:path: TryConvertCdkStack/TryConvertCdkQueue/TryConvertCdkStackTryConvertCdkTopic4C9C531F/Resource
+    Properties:
+      Endpoint: !GetAtt TryConvertCdkQueueA6B3948A.Arn
+      Protocol: sqs
+      TopicArn: !Ref TryConvertCdkTopic2CABFDF4
+
+  TryConvertCdkTopic2CABFDF4:
+    Type: AWS::SNS::Topic
+    Metadata:
+      aws:cdk:path: TryConvertCdkStack/TryConvertCdkTopic/Resource
+
+  TryConvertCdkACustomResourceABCDEFGH:
+    Type: Custom::CustomThing
+    Properties:
+      ServiceToken: foo
+
+  TryConvertCdkAnotherCustomResourceABCDEFGI:
+    Type: Custom::AnotherCustomThing
+    Properties:
+      ServiceToken: foo
+
+  TryConvertCdkAnotherCustomResource2ABCDEFGJ:
+    Type: Custom::AnotherCustomThing
+    Properties:
+      ServiceToken: foo
+      FixThisJoin: 
+        Fn::Join:
+          - ""
+          - - "arn:"
+            - Ref: AWS::Partition
+            - ":logs:"
+            - Ref: AWS::Region
+            - ":"
+            - Ref: AWS::AccountId
+            - ":"
+            - Fn::GetAtt:
+              - "Foo"
+              - "Bar"
+
+  CDKMetadata:
+    Type: AWS::CDK::Metadata
+    Metadata:
+      aws:cdk:path: TryConvertCdkStack/CDKMetadata/Default
+    Properties:
+      Analytics: v2:deflate64:H4sIAAAAAAAA/1WNQQqDMBREz+I+/hop9ABeoNXuS0xS+GoT9SeVEnL3kgQK3cy8GQamBX45A6/EQbVUc73gCGFwQs5MHPQItBGEm9des+5pCmS92gXl51eWGBkZgjD4keSOq0Nr0uIv3+2KMrUZYkzYa7J+l/mjs0ZhWkZmrNIw0endNsBbaKqJEOvdG4cvDX3xL90E7lHCAAAA
+    Condition: CDKMetadataAvailable
+
+Outputs:
+  TryConvertCdkQueueA6B3948CName:
+    Value: !Ref TryConvertCdkQueueA6B3948C