Repository: aws-cloudformation/rain (go)

Issue:
build command produces incorrect output for resource type AWS::ECR::ReplicationConfiguration but only in non-bare mode
```
$ rain --version
Rain v1.15.0 linux/amd64
```

AFAICT, bare output is correct:
```
$ rain build --bare AWS::ECR::ReplicationConfiguration
AWSTemplateFormatVersion: 2010-09-09

Description: Generated by rain

Resources:
  ReplicationConfiguration:
    Type: AWS::ECR::ReplicationConfiguration
    Properties:
      ReplicationConfiguration:
        Rules:
          - Destinations:
              - Region: '[0-9a-z-]{2,25}'
                RegistryId: ^[0-9]{12}$
              - Region: '[0-9a-z-]{2,25}'
                RegistryId: ^[0-9]{12}$
          - Destinations:
              - Region: '[0-9a-z-]{2,25}'
                RegistryId: ^[0-9]{12}$
              - Region: '[0-9a-z-]{2,25}'
                RegistryId: ^[0-9]{12}$
```

Non-bare output is incorrect.  In particular, notice that the required `RegistryId` key is missing:
```
AWSTemplateFormatVersion: 2010-09-09

Description: Generated by rain

Resources:
  ReplicationConfiguration:
    Type: AWS::ECR::ReplicationConfiguration
    Properties:
      ReplicationConfiguration:
        Rules:
          - RepositoryFilters:
              - Filter: ^(?:[a-z0-9]+(?:[._-][a-z0-9]*)*/)*[a-z0-9]*(?:[._-][a-z0-9]*)*$
                FilterType: PREFIX_MATCH
              - Filter: ^(?:[a-z0-9]+(?:[._-][a-z0-9]*)*/)*[a-z0-9]*(?:[._-][a-z0-9]*)*$
                FilterType: PREFIX_MATCH
            Destinations:
              - Region: '[0-9a-z-]{2,25}'
              - Region: '[0-9a-z-]{2,25}'
          - RepositoryFilters:
              - Filter: ^(?:[a-z0-9]+(?:[._-][a-z0-9]*)*/)*[a-z0-9]*(?:[._-][a-z0-9]*)*$
                FilterType: PREFIX_MATCH
              - Filter: ^(?:[a-z0-9]+(?:[._-][a-z0-9]*)*/)*[a-z0-9]*(?:[._-][a-z0-9]*)*$
                FilterType: PREFIX_MATCH
            Destinations:
              - Region: '[0-9a-z-]{2,25}'
              - Region: '[0-9a-z-]{2,25}'
```

Pull request:
Fix skipping read only props by full path
Fixes #523 

Patch:
diff --git a/internal/cmd/build/build.go b/internal/cmd/build/build.go
index 2944a8c4..2d1fc6f8 100644
--- a/internal/cmd/build/build.go
+++ b/internal/cmd/build/build.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"os"
 	"slices"
+	"sort"
 	"strings"
 
 	"github.com/aws-cloudformation/rain/cft"
@@ -187,11 +188,20 @@ func buildProp(n *yaml.Node, propName string, prop cfn.Prop, schema cfn.Schema,
 	return nil
 }
 
+func sortKeys(m map[string]*cfn.Prop) []string {
+	keys := make([]string, len(m))
+	i := 0
+	for k := range m {
+		keys[i] = k
+		i++
+	}
+	sort.Strings(keys)
+	return keys
+}
+
 // buildNode recursively builds a node for a schema-like object
 func buildNode(n *yaml.Node, s cfn.SchemaLike, schema *cfn.Schema, ancestorTypes []string, bare bool) error {
 
-	// Add all props or just the required ones
-	// TODO: Bug - we need them all, just don't output anything... ?
 	if bare {
 		requiredProps := s.GetRequired()
 		props := s.GetProperties()
@@ -208,8 +218,16 @@ func buildNode(n *yaml.Node, s cfn.SchemaLike, schema *cfn.Schema, ancestorTypes
 			}
 		}
 	} else {
-		for k, p := range s.GetProperties() {
-			propPath := "/properties/" + k
+		props := s.GetProperties()
+		// Sort the properties so we get consistent output
+		sortedKeys := sortKeys(props)
+		for _, k := range sortedKeys {
+			p := props[k]
+			propPath := "/properties/"
+			for _, ancestor := range ancestorTypes {
+				propPath = ancestor + "/"
+			}
+			propPath += k
 			// Don't emit read-only properties
 			if slices.Contains(schema.ReadOnlyProperties, propPath) {
 				continue


Test patch:
diff --git a/internal/cmd/build/build_test.go b/internal/cmd/build/build_test.go
index 607e39ec..32284245 100644
--- a/internal/cmd/build/build_test.go
+++ b/internal/cmd/build/build_test.go
@@ -68,3 +68,43 @@ func TestBareMetricFilter(t *testing.T) {
 	path := SCHEMAS + "aws-logs-metricfilter.json"
 	fromFile(path, "AWS::Logs::MetricFilter", "MyMetricFilter", true, t)
 }
+
+func Example_omit_read_only() {
+	os.Args = []string{
+		os.Args[0],
+		"AWS::ECR::ReplicationConfiguration",
+	}
+	Cmd.Execute()
+	// Output:
+	// AWSTemplateFormatVersion: 2010-09-09
+	//
+	// Description: Generated by rain
+	//
+	// Resources:
+	//   ReplicationConfiguration:
+	//     Type: AWS::ECR::ReplicationConfiguration
+	//     Properties:
+	//       ReplicationConfiguration:
+	//         Rules:
+	//           - Destinations:
+	//               - Region: '[0-9a-z-]{2,25}'
+	//                 RegistryId: ^[0-9]{12}$
+	//               - Region: '[0-9a-z-]{2,25}'
+	//                 RegistryId: ^[0-9]{12}$
+	//             RepositoryFilters:
+	//               - Filter: ^(?:[a-z0-9]+(?:[._-][a-z0-9]*)*/)*[a-z0-9]*(?:[._-][a-z0-9]*)*$
+	//                 FilterType: PREFIX_MATCH
+	//               - Filter: ^(?:[a-z0-9]+(?:[._-][a-z0-9]*)*/)*[a-z0-9]*(?:[._-][a-z0-9]*)*$
+	//                 FilterType: PREFIX_MATCH
+	//           - Destinations:
+	//               - Region: '[0-9a-z-]{2,25}'
+	//                 RegistryId: ^[0-9]{12}$
+	//               - Region: '[0-9a-z-]{2,25}'
+	//                 RegistryId: ^[0-9]{12}$
+	//             RepositoryFilters:
+	//               - Filter: ^(?:[a-z0-9]+(?:[._-][a-z0-9]*)*/)*[a-z0-9]*(?:[._-][a-z0-9]*)*$
+	//                 FilterType: PREFIX_MATCH
+	//               - Filter: ^(?:[a-z0-9]+(?:[._-][a-z0-9]*)*/)*[a-z0-9]*(?:[._-][a-z0-9]*)*$
+	//                 FilterType: PREFIX_MATCH
+
+}