Repository: aws-cloudformation/rain
Pull request #675 (2025-04-07 19:52:58)

Add S3 URI handling to modules


Patch:
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
index ad0c19cb..fce51bab 100644
--- a/.pre-commit-hooks.yaml
+++ b/.pre-commit-hooks.yaml
@@ -1,15 +1,6 @@
-- id: rain-format
-  name: rain format
-  description: Format AWS CloudFormation templates
-  entry: rain
+- id: cfn-format
+  name: Format CloudFormation templates
+  entry: cfn-format -w
   language: golang
-  files: \.(yml|yaml|json)$
-  args: ["fmt", "-w"]
-
-- id: rain-verify
-  name: rain verify
-  description: Verify AWS CloudFormation templates formatting without modifying files
-  entry: rain
-  language: golang
-  files: \.(yml|yaml|json)$
-  args: ["fmt", "-v"]
+  files: \.template$
+  description: Format CloudFormation templates
diff --git a/cft/format/transform.go b/cft/format/transform.go
index 4907e45f..ddbaea57 100644
--- a/cft/format/transform.go
+++ b/cft/format/transform.go
@@ -119,16 +119,14 @@ func formatNode(n *yaml.Node) *yaml.Node {
 		if n.Kind == yaml.ScalarNode {
 			n.Style = yaml.DoubleQuotedStyle
 		}
-	case "strict-booleans":
-		// Enforce YAML 1.2 behavior: Only `true` and `false` are booleans, all others are strings
+	case "":
+		// Default style for consistent formatting:
+		// Force double quotes on ambiguous scalar strings.
 		if n.Kind == yaml.ScalarNode && n.Tag == "!!str" && isAmbiguousScalar(n.Value) {
 			n.Style = yaml.DoubleQuotedStyle
 		} else {
 			n.Style = 0
 		}
-	case "":
-		// Default style for consistent formatting
-		n.Style = 0
 	default:
 		panic("invalid --node-style: " + NodeStyle)
 	}
diff --git a/cft/pkg/conditions.go b/cft/pkg/conditions.go
index cf11dd0e..6b099d1a 100644
--- a/cft/pkg/conditions.go
+++ b/cft/pkg/conditions.go
@@ -124,7 +124,7 @@ func evalCond(condName string, condValue interface{}, conditions map[string]inte
 			return evaluateNot(not, conditions, module)
 		}
 		if equals, ok := v["Fn::Equals"]; ok {
-			return evaluateEquals(equals, module)
+			return evaluateEquals(equals)
 		}
 		if condition, ok := v["Condition"]; ok {
 			// Reference to another condition
@@ -210,7 +210,7 @@ func evaluateNot(notExpr interface{}, conditions map[string]interface{}, module
 }
 
 // evaluateEquals evaluates an Fn::Equals condition
-func evaluateEquals(equalsExpr interface{}, module *Module) (bool, error) {
+func evaluateEquals(equalsExpr interface{}) (bool, error) {
 	equalsList, ok := equalsExpr.([]interface{})
 	if !ok || len(equalsList) != 2 {
 		return false, fmt.Errorf("Fn::Equals requires exactly two values")
diff --git a/cft/pkg/content.go b/cft/pkg/content.go
index 104f84e2..2f827472 100644
--- a/cft/pkg/content.go
+++ b/cft/pkg/content.go
@@ -17,33 +17,21 @@ type ModuleContent struct {
 	BaseUri    string
 }
 
-// Helper function to handle zip file extraction with proper path resolution
-func handleZipFile(root string, location string, hash string, path string) ([]byte, error) {
-	// Resolve the path to the zip file if it's a local path
-	zipPath := location
-	if !strings.HasPrefix(zipPath, "http://") && !strings.HasPrefix(zipPath, "https://") {
-		// If it's a relative path, resolve it relative to the template's directory
-		if !filepath.IsAbs(zipPath) {
-			zipPath = filepath.Join(root, zipPath)
-		}
-	}
+func isHttpsUrl(uri string) bool {
+	return strings.HasPrefix(uri, "https://")
+}
 
-	// Check if the zip file exists if it's a local file
-	if !strings.HasPrefix(zipPath, "http://") && !strings.HasPrefix(zipPath, "https://") {
-		_, err := os.Stat(zipPath)
-		if err != nil {
-			return nil, fmt.Errorf("error accessing zip file %s: %v", zipPath, err)
-		}
-	}
+func isS3URI(uri string) bool {
+	return strings.HasPrefix(uri, "s3://")
+}
 
-	// Unzip, verify hash if there is one, and put the files in memory
-	content, err := DownloadFromZip(zipPath, hash, path)
-	if err != nil {
-		config.Debugf("ZIP: Error extracting from zip: %v", err)
-		return nil, err
+// resolveZipLocation ensures that local zip paths are resolved relative to the template's directory
+func resolveZipLocation(root string, zipLocation string) string {
+	// For local files, resolve the path relative to the template's directory
+	if !isS3URI(zipLocation) && !isHttpsUrl(zipLocation) && !filepath.IsAbs(zipLocation) {
+		return filepath.Join(root, zipLocation)
 	}
-
-	return content, nil
+	return zipLocation
 }
 
 // Get the module's content from a local file, memory, or a remote uri
@@ -54,6 +42,8 @@ func getModuleContent(
 	baseUri string,
 	uri string) (*ModuleContent, error) {
 
+	config.Debugf("getModuleContent root: %s, uri: %s", root, uri)
+
 	var content []byte
 	var err error
 	var newRootDir string
@@ -71,7 +61,8 @@ func getModuleContent(
 
 					if strings.HasSuffix(packageAlias.Location, ".zip") {
 						isZip = true
-						content, err = handleZipFile(root, packageAlias.Location, packageAlias.Hash, path)
+						zipLocation := resolveZipLocation(root, packageAlias.Location)
+						content, err = DownloadFromZip(zipLocation, packageAlias.Hash, path)
 						if err != nil {
 							return nil, err
 						}
@@ -92,12 +83,21 @@ func getModuleContent(
 	// getModuleContent: root=cft/pkg/tmpl/awscli-modules, baseUri=, uri=package.zip/zip-module.yaml
 	if strings.Contains(uri, ".zip/") {
 		isZip = true
-		tokens := strings.Split(uri, "/")
-		location := tokens[0]
-		path := strings.Join(tokens[1:], "/")
-		content, err = handleZipFile(root, location, "", path)
-		if err != nil {
-			return nil, err
+		
+		// Extract the zip location and path within the zip
+		zipIndex := strings.Index(uri, ".zip/")
+		if zipIndex > 0 {
+			zipLocation := uri[:zipIndex+4]  // Include the .zip part
+			zipPath := uri[zipIndex+5:]      // Skip the .zip/ part
+			
+			zipLocation = resolveZipLocation(root, zipLocation)
+			config.Debugf("Extracting from zip: %s, path: %s", zipLocation, zipPath)
+			
+			// Use DownloadFromZip directly - it can handle S3, HTTPS, and local files
+			content, err = DownloadFromZip(zipLocation, "", zipPath)
+			if err != nil {
+				return nil, err
+			}
 		}
 	}
 
@@ -109,7 +109,8 @@ func getModuleContent(
 			path := strings.Replace(uri, packageAlias.Alias+"/", "", 1)
 			if strings.HasSuffix(packageAlias.Location, ".zip") {
 				isZip = true
-				content, err = handleZipFile(root, packageAlias.Location, packageAlias.Hash, path)
+				zipLocation := resolveZipLocation(root, packageAlias.Location)
+				content, err = DownloadFromZip(zipLocation, packageAlias.Hash, path)
 				if err != nil {
 					return nil, err
 				}
@@ -122,7 +123,7 @@ func getModuleContent(
 	// Is this a local file or a URL or did we already unzip a package?
 	if isZip {
 		config.Debugf("Using content from a zipped module package (length: %d bytes)", len(content))
-	} else if strings.HasPrefix(uri, "https://") {
+	} else if isHttpsUrl(uri) || isS3URI(uri) {
 		config.Debugf("Downloading from URL: %s", uri)
 		content, err = downloadModule(uri)
 		if err != nil {
@@ -138,6 +139,7 @@ func getModuleContent(
 		baseUri = strings.Join(urlParts[:len(urlParts)-1], "/")
 
 	} else {
+		config.Debugf("Downloading from a local file, baseUri=%s, uri=%s", baseUri, uri)
 		if baseUri != "" {
 			// If we have a base URL, prepend it to the relative path
 			uri = baseUri + "/" + uri
diff --git a/cft/pkg/download.go b/cft/pkg/download.go
index f7db696b..9d54246d 100644
--- a/cft/pkg/download.go
+++ b/cft/pkg/download.go
@@ -11,6 +11,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/aws-cloudformation/rain/internal/aws/s3"
 	"github.com/aws-cloudformation/rain/internal/config"
 	"github.com/google/uuid"
 )
@@ -42,11 +43,25 @@ func downloadHash(uri string) (string, error) {
 
 // DownloadFromZip retrieves a single file from a zip file hosted on a URI
 func DownloadFromZip(uriString string, verifyHash string, path string) ([]byte, error) {
+
+	config.Debugf("DownloadFromZip uriString: %s, path: %s",
+		uriString, path)
+
 	var zipData []byte
 	var err error
-	
-	// Check if it's a URL or local file
-	if strings.HasPrefix(uriString, "http://") || strings.HasPrefix(uriString, "https://") {
+
+	isUrl := isHttpsUrl(uriString)
+	isS3 := isS3URI(uriString)
+
+	// Check if it's an S3 URI, HTTPS URL, or local file
+	if isS3 {
+		// Download from S3
+		config.Debugf("Downloading from S3: %s", uriString)
+		zipData, err = downloadS3(uriString)
+		if err != nil {
+			return nil, fmt.Errorf("failed to download zip from S3: %v", err)
+		}
+	} else if isUrl {
 		// Download from URL
 		config.Debugf("Downloading %s", uriString)
 		resp, err := http.Get(uriString)
@@ -59,7 +74,7 @@ func DownloadFromZip(uriString string, verifyHash string, path string) ([]byte,
 				config.Debugf("Error closing body: %v", err)
 			}
 		}(resp.Body)
-		
+
 		zipData, err = io.ReadAll(resp.Body)
 		if err != nil {
 			return nil, err
@@ -112,7 +127,7 @@ func DownloadFromZip(uriString string, verifyHash string, path string) ([]byte,
 
 		// Download or read the hash
 		var originalHash string
-		if strings.HasPrefix(verifyHash, "http://") || strings.HasPrefix(verifyHash, "https://") {
+		if isUrl {
 			originalHash, err = downloadHash(verifyHash)
 			if err != nil {
 				return nil, err
@@ -215,9 +230,31 @@ func Unzip(f *os.File, dest string) error {
 	return nil
 }
 
+func downloadS3(uri string) ([]byte, error) {
+	// Parse the S3 URI
+	bucket, key, err := s3.ParseURI(uri)
+	if err != nil {
+		return nil, err
+	}
+
+	// Download the file from S3
+	content, err := s3.GetObject(bucket, key)
+	if err != nil {
+		return nil, err
+	}
+
+	return content, nil
+}
+
 // downloadModule downloads the file from the given URI and returns its content as a byte slice.
 func downloadModule(uri string) ([]byte, error) {
 	config.Debugf("Downloading %s", uri)
+
+	// If it's an S3 uri, use the s3 package to download the file
+	if strings.HasPrefix(uri, "s3://") {
+		return downloadS3(uri)
+	}
+
 	resp, err := http.Get(uri)
 	if err != nil {
 		return nil, err
diff --git a/cft/pkg/module.go b/cft/pkg/module.go
index c48db419..dc3acc1d 100644
--- a/cft/pkg/module.go
+++ b/cft/pkg/module.go
@@ -128,11 +128,9 @@ func processModulesSection(t *cft.Template, n *yaml.Node,
 		outputNode := node.MakeMapping()
 
 		err = processModule(
-			name,
 			parsed,
 			outputNode,
 			t,
-			parsed.AsTemplate.Constants,
 			moduleConfig,
 			parentModule)
 		if err != nil {
@@ -151,9 +149,6 @@ func processModulesSection(t *cft.Template, n *yaml.Node,
 			config.Debugf("processModuleSection %s outputNode did not have any Resources", name)
 		}
 
-		config.Debugf("\nparent template after %s processModule ==== \n%s\n",
-			name, node.YamlStr(t.Node))
-
 	}
 
 	// Look for GetAtts like Content[].Arn that reference
@@ -188,11 +183,9 @@ func addScalarAttribute(out *yaml.Node, name string, moduleResource *yaml.Node,
 
 // processModule performs all of the module logic and injects the content into the parent
 func processModule(
-	logicalId string,
 	parsedModule *ParsedModule,
 	outputNode *yaml.Node,
 	t *cft.Template,
-	moduleConstants map[string]*yaml.Node,
 	moduleConfig *cft.ModuleConfig,
 	parentModule *Module) error {
 
@@ -223,8 +216,7 @@ func processModule(
 		},
 	}
 
-	err = processRainSection(moduleAsTemplate,
-		parsedModule.RootDir, parsedModule.FS)
+	err = processRainSection(moduleAsTemplate)
 	if err != nil {
 		return err
 	}
@@ -251,9 +243,6 @@ func processModule(
 		return err
 	}
 
-	config.Debugf("Module %s about to resolve t.Node in processModule",
-		m.Config.Name)
-
 	// Resolve any references to this module in the parent template
 	//err = m.Resolve(t.Node)
 	//if err != nil {
@@ -376,8 +365,6 @@ func (module *Module) ProcessResources(outputNode *yaml.Node) error {
 		// Some refs are to other resources in the module
 		// Other refs are to the module's parameters
 
-		config.Debugf("%s about to resolve resource %s", module.Config.Name, nameNode.Value)
-
 		err = module.Resolve(clonedResource)
 		if err != nil {
 			return fmt.Errorf("failed to resolve refs: %v", err)
@@ -403,13 +390,6 @@ func (module *Module) ProcessResources(outputNode *yaml.Node) error {
 		// if we try to resolve it again later.
 		module.ParentTemplate.AddResolvedModuleNode(clonedResource)
 
-		parentName := ""
-		if module.ParentModule != nil {
-			parentName = module.ParentModule.Config.Name
-		}
-		config.Debugf("Module %s (p:%s) adding resource:\n%s\n", module.Config.Name,
-			parentName,
-			node.YamlStr(clonedResource))
 	}
 
 	return nil
@@ -422,7 +402,6 @@ func processRainResourceModule(
 	outputNode *yaml.Node,
 	t *cft.Template,
 	parent node.NodePair,
-	moduleConstants map[string]*yaml.Node,
 	source string,
 	parsed *ParsedModule) error {
 
@@ -455,7 +434,7 @@ func processRainResourceModule(
 	}
 	moduleConfig.Source = source
 
-	return processModule(logicalId, parsed, outputNode, t, moduleConstants, moduleConfig, nil)
+	return processModule(parsed, outputNode, t, moduleConfig, nil)
 }
 
 func checkPackageAlias(t *cft.Template, uri string) *cft.PackageAlias {
@@ -551,7 +530,7 @@ func module(ctx *directiveContext) (bool, error) {
 	}
 
 	// This needs to happen before recursing, since sub-modules need resolved constants in the parent
-	err = processRainSection(moduleAsTemplate, moduleContent.NewRootDir, ctx.fs)
+	err = processRainSection(moduleAsTemplate)
 	if err != nil {
 		return false, err
 	}
@@ -570,8 +549,7 @@ func module(ctx *directiveContext) (bool, error) {
 
 	// Create a new node to represent the parsed module
 	var outputNode yaml.Node
-	err = processRainResourceModule(moduleNode,
-		&outputNode, t, parent, moduleAsTemplate.Constants, uri, parsed)
+	err = processRainResourceModule(moduleNode, &outputNode, t, parent, uri, parsed)
 	if err != nil {
 		config.Debugf("processModule error: %v, moduleNode: %s", err, node.ToSJson(moduleNode))
 		return false, fmt.Errorf("failed to process module %s: %v", uri, err)
diff --git a/cft/pkg/pkg.go b/cft/pkg/pkg.go
index 9890963c..dac5025f 100644
--- a/cft/pkg/pkg.go
+++ b/cft/pkg/pkg.go
@@ -106,7 +106,7 @@ func transform(ctx *transformContext) (bool, error) {
 
 // processRainSection returns true if the Rain section of the template existed
 // The section is removed by this function
-func processRainSection(t *cft.Template, rootDir string, fs *embed.FS) error {
+func processRainSection(t *cft.Template) error {
 	t.Constants = make(map[string]*yaml.Node)
 	rainNode, err := t.GetSection(cft.Rain)
 	if err != nil {
@@ -139,7 +139,7 @@ func Template(t *cft.Template, rootDir string, fs *embed.FS) (*cft.Template, err
 	var err error
 
 	// First look for a Rain section and store constants
-	err = processRainSection(t, rootDir, fs)
+	err = processRainSection(t)
 	if err != nil {
 		return nil, fmt.Errorf("failed to process Rain section: %v", err)
 	}
diff --git a/cft/pkg/resolve.go b/cft/pkg/resolve.go
index 536f0f37..943282b9 100644
--- a/cft/pkg/resolve.go
+++ b/cft/pkg/resolve.go
@@ -24,13 +24,8 @@ func (module *Module) Resolve(n *yaml.Node) error {
 	vf := func(v *visitor.Visitor) {
 		vn := v.GetYamlNode()
 
-		if vn.Value != "" {
-			config.Debugf("Resolve %s", vn.Value)
-		}
-
 		if vn == module.Config.Node {
 			// Don't resolve my own config
-			config.Debugf("Resolve skipping self:\n%s\n", node.YamlStr(vn))
 			v.SkipChildren()
 			return
 		}
@@ -44,7 +39,6 @@ func (module *Module) Resolve(n *yaml.Node) error {
 		if module.ParentTemplate.ModuleAlreadyResolved(vn) {
 			// If we marked a node as resolved, skip all
 			// of its child nodes
-			config.Debugf("Resolve skipping:\n%s\n", node.YamlStr(vn))
 			v.SkipChildren()
 			return
 		}
@@ -182,8 +176,6 @@ func (module *Module) resolveParam(params *yaml.Node, n *yaml.Node, parentProps
 // ${Foo.Bar} is treated like a GetAtt.
 func (module *Module) ResolveSub(n *yaml.Node) error {
 
-	original := node.Clone(n)
-
 	prop := n.Content[1]
 	words, err := parse.ParseSub(prop.Value, true)
 	if err != nil {
@@ -256,10 +248,6 @@ func (module *Module) ResolveSub(n *yaml.Node) error {
 
 	*n = *newProp
 
-	config.Debugf("ResolveSub:\n%s\n -> \n%s\n",
-		node.YamlStr(original),
-		node.YamlStr(n))
-
 	return nil
 }
 
diff --git a/cft/pkg/tmpl/awscli-modules/s3-template.yaml b/cft/pkg/tmpl/awscli-modules/s3-template.yaml
new file mode 100644
index 00000000..5000c07e
--- /dev/null
+++ b/cft/pkg/tmpl/awscli-modules/s3-template.yaml
@@ -0,0 +1,19 @@
+Packages:
+  abc:
+    Source: s3://ezbeard-awscli/modules/package.zip
+Constants:
+  ModuleSource: s3://ezbeard-awscli/modules
+Modules:
+  Content:
+    Source: !Sub ${Const::ModuleSource}/basic-module.yaml  
+    Properties:
+      Name: foo
+    Overrides:
+      Bucket:
+        Properties:
+          OverrideMe: def
+  TestPackage:
+    Source: $abc/zip-module.yaml
+Resources:
+  OtherResource:
+    Type: AWS::S3::Bucket
diff --git a/go.mod b/go.mod
index 462e3628..88e0daa7 100644
--- a/go.mod
+++ b/go.mod
@@ -87,7 +87,6 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
diff --git a/go.sum b/go.sum
index bf5e16e6..911a2d3c 100644
--- a/go.sum
+++ b/go.sum
@@ -4,140 +4,72 @@ github.com/appscode/jsonpatch v1.0.1 h1:e82Bj+rsBSnpsmjiIGlc9NiKSBpJONZkamk/F8Gr
 github.com/appscode/jsonpatch v1.0.1/go.mod h1:4AJxUpXUhv4N+ziTvIcWWXgeorXpxPZOfk9HdEVr96M=
 github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI=
 github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
-github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo=
-github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
 github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
 github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
-github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg=
-github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg=
 github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=
 github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
-github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27 h1:AmB5QxnD+fBFrg9LcqzkgF/CaYvMyU/BTlejG4t1S7Q=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27/go.mod h1:Sai7P3xTiyv9ZUYO3IFxMnmiIP759/67iQbU4kdmkyU=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
-github.com/aws/aws-sdk-go-v2/service/acm v1.30.10 h1:zjtYFhXDtAYNMvmB6WN9z6EvwG1NwROU5VZL/A2851s=
-github.com/aws/aws-sdk-go-v2/service/acm v1.30.10/go.mod h1:L2gQdSBABO9hWudBdfRDlxkKNhhUReGFhy6yIwit7aA=
 github.com/aws/aws-sdk-go-v2/service/acm v1.31.1 h1:FB1PgU6vlXbqehxZiHuYQRWo5Ou6sQrFJcUaRe27lRo=
 github.com/aws/aws-sdk-go-v2/service/acm v1.31.1/go.mod h1:3sKYAgRbuBa2QMYGh/WEclwnmfx+QoPhhX25PdSQSQM=
-github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.23.2 h1:8CcCVDj3hdUJoa1aOxdsKl6c73bC80x9ZylUWCytmgk=
-github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.23.2/go.mod h1:pCst69koE8+hbZ7EohPkOrOhyvqWqXxIVo8cp655yAg=
 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.28.0 h1:Lh4LitQr5CiWdWExkT+5Qrc1HA/fNJpsO4yO0dNurbg=
 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.28.0/go.mod h1:0b5Rq7rUvSQFYHI1UO0zFTV/S6j6DUyuykXA80C+YOI=
-github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.23.4 h1:SutEGaEEJbOaXO90HBQ42flkaQs4L5eJSvT4Zsaumk0=
-github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.23.4/go.mod h1:EW4j0ChF7B97eNaTM7A+878ErjGdrdggogtBjBapQe4=
 github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.24.1 h1:pjX6Yr5YZKHC9jxSV0N8aOuB4BPHuovnta96aAoxvog=
 github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.24.1/go.mod h1:ifQSgXMoHWzSB1gBIqKPDqXkp9TP/a/fmx0AIRFHVL0=
-github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.4 h1:uH6So7Ee+2JQf+TKbfifXKUDNN0JfaJ6CgJ6Bh/u1sc=
-github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.4/go.mod h1:GdDLBO8SzD4wvQ6fhqU1QCmvG1waj1MPHL4cBtuSgdQ=
 github.com/aws/aws-sdk-go-v2/service/cloudformation v1.59.0 h1:5ToD3Yv6arXLYk3GZZHVjGX4rqWdfWldh3/CkPX6JPA=
 github.com/aws/aws-sdk-go-v2/service/cloudformation v1.59.0/go.mod h1:penaZKzGmqHGZId4EUCBIW/f9l4Y7hQ5NKd45yoCYuI=
-github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.2 h1:2BWjTxlOYCKE3+j4xFmVybChyRinvBG5mdSmodB9CPY=
-github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.2/go.mod h1:JaXaFuXF59JpQIDhR3Fj5ZFhB5TGp7MZnIF9f4nYvmk=
 github.com/aws/aws-sdk-go-v2/service/cloudfront v1.45.2 h1:S3JpsBLyn/jqSJ6GgsbDQHubmop6fshQk/iOaOeotsc=
 github.com/aws/aws-sdk-go-v2/service/cloudfront v1.45.2/go.mod h1:FIBJ48TS+qJb+Ne4qJ+0NeIhtPTVXItXooTeNeVI4Po=
-github.com/aws/aws-sdk-go-v2/service/codeartifact v1.33.9 h1:KafLwAM4bu+ItGB1wtDJJKp7N9syPlhcYXsMAalr3cA=
-github.com/aws/aws-sdk-go-v2/service/codeartifact v1.33.9/go.mod h1:vKxoyommaiAMRIB7BiPBWindIT06gnOWQM3Xci/JxIo=
 github.com/aws/aws-sdk-go-v2/service/codeartifact v1.34.1 h1:7kG4zaTjHxWRkU+CZSgdPM2Yv9aLYJTbp99IVwbWN3Q=
 github.com/aws/aws-sdk-go-v2/service/codeartifact v1.34.1/go.mod h1:QPTNJjlY2i7XZhMDb7vX3Hxg2YtLucSU4kzDYxXm3k4=
-github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.2 h1:XcdIh35yg1J8bAiUOLtL/PoPMSGsD72Zanwmim8jEXc=
-github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.2/go.mod h1:516U/KQM3zdcahNBjHUZKGWNfNnIYyt7sxLeqOx78b0=
 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.42.0 h1:EJXx6zb+lOe/Do2bO0d0dwVnIRGoP5J5xZ0BTn3LbqM=
 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.42.0/go.mod h1:yYaWRnVSPyAmexW5t7G3TcuYoalYfT+xQwzWsvtUQ7M=
-github.com/aws/aws-sdk-go-v2/service/ec2 v1.198.3 h1:h5UPeMBMm29Vjk45QVnH2Qu2QMbzRrWUORwyGjzWQso=
-github.com/aws/aws-sdk-go-v2/service/ec2 v1.198.3/go.mod h1:WAFpTnWeO2BNfwpQ8LTTTx9l9/bTztMPrA8gkh41PvI=
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.211.0 h1:+5SxE8y8TIOYt8cwoqtd4WVpdpHHDWXD99DEAIjfBJ8=
 github.com/aws/aws-sdk-go-v2/service/ec2 v1.211.0/go.mod h1:ouvGEfHbLaIlWwpDpOVWPWR+YwO0HDv3vm5tYLq8ImY=
-github.com/aws/aws-sdk-go-v2/service/iam v1.38.4 h1:440YtmP8Cn6Qp7WHYfvz2/Xzmu1v1Vox/FJnzUDDQGM=
-github.com/aws/aws-sdk-go-v2/service/iam v1.38.4/go.mod h1:oXqc4hmGhZpj06Zu8z+ahXhdbjq4Uw8pjN9flty0Ync=
 github.com/aws/aws-sdk-go-v2/service/iam v1.41.0 h1:YvQjxKmA7fNnmphNBQ05PGGsYGYWBi9yWfuXBTKVdPs=
 github.com/aws/aws-sdk-go-v2/service/iam v1.41.0/go.mod h1:mPJkGQzeCoPs82ElNILor2JzZgYENr4UaSKUT8K27+c=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8 h1:iwYS40JnrBeA9e9aI5S6KKN4EB2zR4iUVYN0nwVivz4=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8/go.mod h1:Fm9Mi+ApqmFiknZtGpohVcBGvpTu542VC4XO9YudRi0=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
-github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.8 h1:h56mLNgpqWIL7RZOIQO634Xr569bXGTlIE83t/a0LSE=
-github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.8/go.mod h1:kK04550Xx95KI0sNmwoB7ciS9QkRwt9TojhoTMXyJdo=
 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.15 h1:M1R1rud7HzDrfCdlBQ7NjnRsDNEhXO/vGhuD189Ggmk=
 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.15/go.mod h1:uvFKBSq9yMPV4LGAi7N4awn4tLY+hKE35f8THes2mzQ=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8 h1:/Mn7gTedG86nbpjT4QEKsN1D/fThiYe1qvq7WsBGNHg=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8/go.mod h1:Ae3va9LPmvjj231ukHB6UeT8nS7wTPfC3tMZSZMwNYg=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
-github.com/aws/aws-sdk-go-v2/service/kms v1.37.10 h1:nqYgJ+twjn6hrhTS97j3tlpNXrw4E9N2zQBgw2FAQMg=
-github.com/aws/aws-sdk-go-v2/service/kms v1.37.10/go.mod h1:wHYtyttsH+A6d2MzXYl8cIf4O2Kw1Kg0qzromSX/wOs=
 github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc=
 github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.9 h1:34o9AstfORuhF40AJGBtHvwJB8g4eeHtJlNdD3IN0jY=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.9/go.mod h1:HDPHlS7H2EnMaakVPKQgZbHOxoSJKD9R9ftMeUKoJl8=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 h1:0j58UseBtLuBcP6nY2z4SM1qZEvLF0ylyH6+ggnphLg=
 github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1/go.mod h1:Qy22QnQSdHbZwMZrarsWZBIuK51isPlkD+Z4sztxX0o=
-github.com/aws/aws-sdk-go-v2/service/rds v1.93.4 h1:7+aSHrS6JJHK9/3MpCwBSvbRMvXEdzB+R4ajfTPjDAo=
-github.com/aws/aws-sdk-go-v2/service/rds v1.93.4/go.mod h1:uIyrtXKiRZAHJYgs6LbLd4YTQf1L4Wy9P7AnoNbZAZc=
 github.com/aws/aws-sdk-go-v2/service/rds v1.94.2 h1:KD1evOFbLuxM3THwNpFvANTQZdMfnTTsFDmTDIJItCs=
 github.com/aws/aws-sdk-go-v2/service/rds v1.94.2/go.mod h1:CXiHj5rVyQ5Q3zNSoYzwaJfWm8IGDweyyCGfO8ei5fQ=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2 h1:a7aQ3RW+ug4IbhoQp29NZdc7vqrzKZZfWZSaQAXOZvQ=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2/go.mod h1:xMekrnhmJ5aqmyxtmALs7mlvXw5xRh+eYjOjvrIIFJ4=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.79.0 h1:OIw2nryEApESTYI5deCZGcq4Gvz8DBAt4tJlNyg3v5o=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.79.0/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
-github.com/aws/aws-sdk-go-v2/service/sagemaker v1.172.1 h1:3++lVXBC1ymEmpZTQ9QPWbt6bSeo184mgEUWKjqexhs=
-github.com/aws/aws-sdk-go-v2/service/sagemaker v1.172.1/go.mod h1:2JEk3ogdY++7MjYRJIPdyf5vlIwHb8GfH81tf9ghqyU=
 github.com/aws/aws-sdk-go-v2/service/sagemaker v1.186.0 h1:zvf3JORq+wZ8VbUEZvRVpuweAW/Yy2WlhMZbuSP/XW4=
 github.com/aws/aws-sdk-go-v2/service/sagemaker v1.186.0/go.mod h1:fp2LcfhQkz90js0Bkg5nXdCGCRy4y/FGgc14uvZ97eA=
-github.com/aws/aws-sdk-go-v2/service/servicequotas v1.25.10 h1:jhMvBarCpBxL9/sxIjNY23mtzihUU9bbgzbNz27YWco=
-github.com/aws/aws-sdk-go-v2/service/servicequotas v1.25.10/go.mod h1:WV+4tKbPrBYIwi20IGg4WzHbi2NDpKGTEk6UxwJ7AcE=
 github.com/aws/aws-sdk-go-v2/service/servicequotas v1.26.1 h1:5sfvHajrZLRNoFWlf7I0NpZrr9DEf5XOmuqa6vKIPXQ=
 github.com/aws/aws-sdk-go-v2/service/servicequotas v1.26.1/go.mod h1:oce0GN05LviU4Q1yec1p3ygi+fCaHjLfG1uDuknTHTY=
-github.com/aws/aws-sdk-go-v2/service/ssm v1.56.4 h1:oXh/PjaKtStu7RkaUtuKX6+h/OxXriMa9WyQQhylKG0=
-github.com/aws/aws-sdk-go-v2/service/ssm v1.56.4/go.mod h1:IiHGbiFg4wVdEKrvFi/zxVZbjfEpgSe21N9RwyQFXCU=
 github.com/aws/aws-sdk-go-v2/service/ssm v1.58.0 h1:zQz6Q5uaC8s9734DV9UDAm2q1TEEfOvEejDBSulOapI=
 github.com/aws/aws-sdk-go-v2/service/ssm v1.58.0/go.mod h1:PUWUl5MDiYNQkUHN9Pyd9kgtA/YhbxnSnHP+yQqzrM8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY=
 github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E=
 github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
 github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
-github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
-github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
 github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -149,14 +81,11 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
 github.com/evanphx/json-patch v4.0.0+incompatible h1:xregGRMLBeuRcwiOTHRCsPPuzCQlqhxUPbqdw+zNkLc=
@@ -165,8 +94,6 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
 github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
-github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
-github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -177,10 +104,6 @@ github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
 github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
-github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
 github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
 github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
@@ -189,8 +112,6 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG
 github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
 github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
 github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
-github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo=
-github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
 github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4/qc=
 github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is=
 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
@@ -213,12 +134,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -233,32 +150,21 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
 golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
-golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
-golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
 golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
-golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
-golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
 golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/aws/s3/s3.go b/internal/aws/s3/s3.go
index 8e0f99f9..d469bd87 100644
--- a/internal/aws/s3/s3.go
+++ b/internal/aws/s3/s3.go
@@ -295,9 +295,52 @@ func RainBucket(forceCreation bool) string {
 	return bucketName
 }
 
+// ParseURI parses an S3 URI like s3://bucket/key
+// The object key name is a sequence of Unicode characters with UTF-8 encoding of up to 1,024 bytes long.
+// The bucket name must be a valid DNS name and follow S3 bucket naming rules.
+func ParseURI(uri string) (bucket string, key string, err error) {
+	// Check if the URI starts with s3:// prefix
+	if !strings.HasPrefix(uri, "s3://") {
+		err = fmt.Errorf("invalid s3 uri: %s, must start with s3://", uri)
+		return
+	}
+	
+	// Remove the s3:// prefix
+	uri = strings.TrimPrefix(uri, "s3://")
+	
+	// Split the remaining string by the first /
+	parts := strings.SplitN(uri, "/", 2)
+	if len(parts) == 0 || parts[0] == "" {
+		err = fmt.Errorf("invalid s3 uri: %s, bucket name missing", uri)
+		return
+	}
+	
+	bucket = parts[0]
+	
+	// Validate bucket name (basic validation)
+	if len(bucket) < 3 || len(bucket) > 63 {
+		err = fmt.Errorf("invalid bucket name: %s, length must be between 3 and 63 characters", bucket)
+		return
+	}
+	
+	if len(parts) == 1 {
+		key = ""
+	} else {
+		key = parts[1]
+		
+		// Validate key length (S3 limit is 1024 bytes)
+		if len(key) > 1024 {
+			err = fmt.Errorf("invalid key: %s, length exceeds 1024 bytes", key)
+			return
+		}
+	}
+	return
+}
+
 // GetObject gets an object by key from an S3 bucket
 func GetObject(bucketName string, key string) ([]byte, error) {
 
+	config.Debugf("GetObject bucket %s, key: %s", bucketName, key)
 	accountId, err := getAccountId()
 	if err != nil {
 		return nil, err
diff --git a/modules/cognito.yaml b/modules/cognito.yaml
index 5dc84360..b3d21d5a 100644
--- a/modules/cognito.yaml
+++ b/modules/cognito.yaml
@@ -49,4 +49,3 @@ Resources:
         - openid
       SupportedIdentityProviders:
         - COGNITO
-
diff --git a/modules/compliant-bucket.yaml b/modules/compliant-bucket.yaml
index 290e8d3e..481be557 100644
--- a/modules/compliant-bucket.yaml
+++ b/modules/compliant-bucket.yaml
@@ -28,6 +28,48 @@ Rain:
     LogBucketArn: "${Rain::S3Arn}${Rain::LogBucketName}"
     ReplicaBucketName: "${AppName}-replicas-${AWS::Region}-${AWS::AccountId}"
 
+Modules:
+
+  BucketAccess:
+    Source: bucket-policy.yaml
+    Properties:
+      PolicyBucketName: !Sub ${AppName}-${AWS::Region}-${AWS::AccountId}
+
+  ReplicaBucketAccess:
+    Source: bucket-policy.yaml
+    Properties:
+      PolicyBucketName: !Sub ${AppName}-replicas-${AWS::Region}-${AWS::AccountId}
+
+  LogBucketAccess:
+    Source: bucket-policy.yaml
+    Properties:
+      PolicyBucketName: !Sub ${Rain::LogBucketName}
+    Overrides:
+      Policy:
+        Properties:
+          PolicyDocument:
+            Statement:
+              - Action: s3:*
+                Condition:
+                  Bool:
+                    aws:SecureTransport: false
+                Effect: Deny
+                Principal:
+                  AWS: '*'
+                Resource:
+                  - !Sub ${Rain::LogBucketArn}
+                  - !Sub ${Rain::LogBucketArn}/*
+              - Action: s3:PutObject
+                Condition:
+                  ArnLike:
+                    aws:SourceArn: !Sub ${Rain::LogBucketArn}/*
+                  StringEquals:
+                    aws:SourceAccount: !Ref AWS::AccountId
+                Effect: Allow
+                Principal:
+                  Service: logging.s3.amazonaws.com
+                Resource:
+                  - !Sub ${Rain::LogBucketArn}/*
 
 Resources:
 
@@ -67,36 +109,6 @@ Resources:
       VersioningConfiguration:
         Status: Enabled
 
-  LogBucketAccess:
-    Type: !Rain::Module "bucket-policy.yaml"
-    Properties:
-      PolicyBucketName: !Sub ${Rain::LogBucketName}
-    Overrides:
-      Policy:
-        Properties:
-          PolicyDocument:
-            Statement:
-              - Action: s3:*
-                Condition:
-                  Bool:
-                    aws:SecureTransport: false
-                Effect: Deny
-                Principal:
-                  AWS: '*'
-                Resource:
-                  - !Sub ${Rain::LogBucketArn}
-                  - !Sub ${Rain::LogBucketArn}/*
-              - Action: s3:PutObject
-                Condition:
-                  ArnLike:
-                    aws:SourceArn: !Sub ${Rain::LogBucketArn}/*
-                  StringEquals:
-                    aws:SourceAccount: !Ref AWS::AccountId
-                Effect: Allow
-                Principal:
-                  Service: logging.s3.amazonaws.com
-                Resource:
-                  - !Sub ${Rain::LogBucketArn}/*
 
   Bucket:
     Type: AWS::S3::Bucket
@@ -131,10 +143,6 @@ Resources:
       VersioningConfiguration:
         Status: Enabled
 
-  BucketAccess:
-    Type: !Rain::Module "bucket-policy.yaml"
-    Properties:
-      PolicyBucketName: !Sub ${AppName}-${AWS::Region}-${AWS::AccountId}
 
   ReplicaBucket:
     Type: AWS::S3::Bucket
@@ -167,10 +175,6 @@ Resources:
       VersioningConfiguration:
         Status: Enabled
 
-  ReplicaBucketAccess:
-    Type: !Rain::Module "bucket-policy.yaml"
-    Properties:
-      PolicyBucketName: !Sub ${AppName}-replicas-${AWS::Region}-${AWS::AccountId}
 
   ReplicationPolicy:
     Type: AWS::IAM::RolePolicy
diff --git a/modules/rest-api.yaml b/modules/rest-api.yaml
index 33d16691..17747141 100644
--- a/modules/rest-api.yaml
+++ b/modules/rest-api.yaml
@@ -34,4 +34,3 @@ Resources:
         - !Ref UserPoolArn
       RestApiId: !Ref Api
       Type: COGNITO_USER_POOLS
-
diff --git a/modules/static-site.yaml b/modules/static-site.yaml
index bd6eaa27..2b98fe6d 100644
--- a/modules/static-site.yaml
+++ b/modules/static-site.yaml
@@ -89,8 +89,9 @@ Resources:
               ExcludedRules:
                 - Name: NoUserAgent_HEADER
 
+Modules:
   Content:
-    Type: !Rain::Module "compliant-bucket.yaml"
+    Source: compliant-bucket.yaml
     Properties:
       AppName: !Sub ${AppName}-content
       EmptyOnDelete: true
@@ -120,7 +121,7 @@ Resources:
             Version: "2012-10-17"
 
   CloudFrontLogs:
-    Type: !Rain::Module "compliant-bucket.yaml"
+    Source: compliant-bucket.yaml
     Properties:
       AppName: !Sub ${AppName}-cflogs
       EmptyOnDelete: true
@@ -130,4 +131,3 @@ Resources:
           OwnershipControls:
             Rules:
               - ObjectOwnership: BucketOwnerPreferred
-


Test patch:
diff --git a/cft/format/format_test.go b/cft/format/format_test.go
index 481a7c4e..4a09fc09 100644
--- a/cft/format/format_test.go
+++ b/cft/format/format_test.go
@@ -801,86 +801,22 @@ Outputs {
 	}
 }
 
-func TestAmbiguousScalars_StrictBooleans_On(t *testing.T) {
-	// Save the original style
-	originalStyle := format.NodeStyle
-	// Restore after test completes
-	defer func() {
-		format.NodeStyle = originalStyle
-	}()
-
-	// Override for this test only
-	format.NodeStyle = "strict-booleans"
-
+func TestAmbiguousScalarOn(t *testing.T) {
 	input := `
-Param1:
-  Default: "ON"
-Param2:
-  Default: "OFF"
-Param3:
-  Default: "Yes"
-Param4:
-  Default: "No"
-Param5:
-  Default: "True"
-Param6:
-  Default: "False"
-Param7:
-  Default: Maybe
-Param8:
-  Default: ON
-Param9:
-  Default: OFF
-Param10:
-  Default: Yes
-Param11:
-  Default: No
-Param12:
-  Default: True
-Param13:
-  Default: False
+Resources:
+  MyResource:
+    Type: AWS::RDS::DBClusterParameterGroup
+    Properties:
+      Parameters:
+        require_secure_transport: "ON"
 `
-	// The ambiguous values (Param1 through Param6) should be rendered with quotes,
-	// while a non-ambiguous value (Param7) may be unquoted.
-	expected := `
-Param1:
-  Default: "ON"
-
-Param2:
-  Default: "OFF"
-
-Param3:
-  Default: "Yes"
-
-Param4:
-  Default: "No"
-
-Param5:
-  Default: "True"
-
-Param6:
-  Default: "False"
-
-Param7:
-  Default: Maybe
-
-Param8:
-  Default: "ON"
-
-Param9:
-  Default: "OFF"
-
-Param10:
-  Default: "Yes"
-
-Param11:
-  Default: "No"
-
-Param12:
-  Default: True
-
-Param13:
-  Default: False
+	// We expect the ambiguous value "ON" to remain quoted in the output.
+	expected := `Resources:
+  MyResource:
+    Type: AWS::RDS::DBClusterParameterGroup
+    Properties:
+      Parameters:
+        require_secure_transport: "ON"
 `
 	template, err := parse.String(input)
 	if err != nil {
@@ -893,76 +829,71 @@ Param13:
 	}
 }
 
-func TestAmbiguousScalars_StrictBooleans_Off(t *testing.T) {
+func TestAmbiguousScalarsInParameters(t *testing.T) {
 	input := `
-Param1:
-  Default: "ON"
-Param2:
-  Default: "OFF"
-Param3:
-  Default: "Yes"
-Param4:
-  Default: "No"
-Param5:
-  Default: "True"
-Param6:
-  Default: "False"
-Param7:
-  Default: Maybe
-Param8:
-  Default: ON
-Param9:
-  Default: OFF
-Param10:
-  Default: Yes
-Param11:
-  Default: No
-Param12:
-  Default: True
-Param13:
-  Default: False
+Parameters:
+  Param1:
+    Default: "ON"
+  Param2:
+    Default: "OFF"
+  Param3:
+    Default: "Yes"
+  Param4:
+    Default: "No"
+  Param5:
+    Default: "True"
+  Param6:
+    Default: "False"
+  Param7:
+    Default: "Maybe"
 `
 	// The ambiguous values (Param1 through Param6) should be rendered with quotes,
 	// while a non-ambiguous value (Param7) may be unquoted.
-	expected := `
-Param1:
-  Default: ON
-
-Param2:
-  Default: OFF
-
-Param3:
-  Default: Yes
-
-Param4:
-  Default: No
-
-Param5:
-  Default: "True"
+	expected := `Parameters:
+  Param1:
+    Default: "ON"
 
-Param6:
-  Default: "False"
+  Param2:
+    Default: "OFF"
 
-Param7:
-  Default: Maybe
+  Param3:
+    Default: "Yes"
 
-Param8:
-  Default: ON
+  Param4:
+    Default: "No"
 
-Param9:
-  Default: OFF
+  Param5:
+    Default: "True"
 
-Param10:
-  Default: Yes
+  Param6:
+    Default: "False"
 
-Param11:
-  Default: No
+  Param7:
+    Default: Maybe
+`
+	template, err := parse.String(input)
+	if err != nil {
+		t.Fatal(err)
+	}
 
-Param12:
-  Default: True
+	actual := format.String(template, format.Options{Unsorted: true})
+	if d := cmp.Diff(strings.TrimSpace(expected), strings.TrimSpace(actual)); d != "" {
+		t.Fatalf("Diff: %s", d)
+	}
+}
 
-Param13:
-  Default: False
+func TestNonAmbiguousScalar(t *testing.T) {
+	input := `
+Resources:
+  MyResource:
+    Properties:
+      example_value: "OnX"
+`
+	// Since "OnX" is not an ambiguous token, it can be rendered without quotes.
+	expected := `Resources:
+  MyResource:
+    Properties:
+      example_value: OnX
 `
 	template, err := parse.String(input)
 	if err != nil {
diff --git a/internal/aws/s3/s3_test.go b/internal/aws/s3/s3_test.go
new file mode 100644
index 00000000..cb96ce58
--- /dev/null
+++ b/internal/aws/s3/s3_test.go
@@ -0,0 +1,125 @@
+package s3
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestParseURI(t *testing.T) {
+	tests := []struct {
+		name        string
+		uri         string
+		wantBucket  string
+		wantKey     string
+		wantErr     bool
+		errContains string
+	}{
+		{
+			name:       "Valid URI with bucket and key",
+			uri:        "s3://mybucket/path/to/object.txt",
+			wantBucket: "mybucket",
+			wantKey:    "path/to/object.txt",
+			wantErr:    false,
+		},
+		{
+			name:       "Valid URI with bucket only",
+			uri:        "s3://mybucket",
+			wantBucket: "mybucket",
+			wantKey:    "",
+			wantErr:    false,
+		},
+		{
+			name:        "Empty URI",
+			uri:         "",
+			wantBucket:  "",
+			wantKey:     "",
+			wantErr:     true,
+			errContains: "invalid s3 uri",
+		},
+		{
+			name:       "URI with empty key",
+			uri:        "s3://mybucket/",
+			wantBucket: "mybucket",
+			wantKey:    "",
+			wantErr:    false,
+		},
+		{
+			name:       "URI with complex key",
+			uri:        "s3://mybucket/folder1/folder2/file.txt",
+			wantBucket: "mybucket",
+			wantKey:    "folder1/folder2/file.txt",
+			wantErr:    false,
+		},
+		{
+			name:       "URI with special characters in key",
+			uri:        "s3://mybucket/path with spaces/file-name_123.txt",
+			wantBucket: "mybucket",
+			wantKey:    "path with spaces/file-name_123.txt",
+			wantErr:    false,
+		},
+		{
+			name:        "URI without s3:// prefix",
+			uri:        "mybucket/path/to/object.txt",
+			wantBucket: "",
+			wantKey:    "",
+			wantErr:    true,
+			errContains: "must start with s3://",
+		},
+		{
+			name:        "URI with invalid bucket name (too short)",
+			uri:        "s3://ab",
+			wantBucket: "",
+			wantKey:    "",
+			wantErr:    true,
+			errContains: "length must be between 3 and 63",
+		},
+		{
+			name:        "URI with invalid bucket name (too long)",
+			uri:        "s3://" + strings.Repeat("a", 64),
+			wantBucket: "",
+			wantKey:    "",
+			wantErr:    true,
+			errContains: "length must be between 3 and 63",
+		},
+		{
+			name:        "URI with key exceeding length limit",
+			uri:        "s3://mybucket/" + strings.Repeat("a", 1025),
+			wantBucket: "",
+			wantKey:    "",
+			wantErr:    true,
+			errContains: "length exceeds 1024 bytes",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotBucket, gotKey, err := ParseURI(tt.uri)
+
+			// Check error cases
+			if tt.wantErr {
+				if err == nil {
+					t.Errorf("ParseURI() expected error but got none")
+					return
+				}
+				if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
+					t.Errorf("ParseURI() error = %v, want error containing %v", err, tt.errContains)
+				}
+				return
+			}
+
+			// Check non-error cases
+			if err != nil {
+				t.Errorf("ParseURI() unexpected error = %v", err)
+				return
+			}
+
+			if gotBucket != tt.wantBucket {
+				t.Errorf("ParseURI() gotBucket = %v, want %v", gotBucket, tt.wantBucket)
+			}
+
+			if gotKey != tt.wantKey {
+				t.Errorf("ParseURI() gotKey = %v, want %v", gotKey, tt.wantKey)
+			}
+		})
+	}
+}
diff --git a/test/webapp/webapp.yaml b/test/webapp/webapp.yaml
index 7a8a5161..ea9f5cd0 100644
--- a/test/webapp/webapp.yaml
+++ b/test/webapp/webapp.yaml
@@ -16,10 +16,9 @@ Parameters:
     Description: This name is used as a prefix for resource names
     Default: rain-webapp
 
-Resources:
-
+Modules:
   Site:
-    Type: !Rain::Module "../../modules/static-site.yaml"
+    Source: ../../modules/static-site.yaml
     Properties:
       AppName: !Ref AppName
     Overrides:
@@ -41,7 +40,7 @@ Resources:
                 - Rain::OutputValue AppClientId
 
   Cognito:
-    Type: !Rain::Module "../../modules/cognito.yaml"
+    Source: ../../modules/cognito.yaml
     Properties:
       AppName: !Ref AppName
       CallbackURL: !Sub "https://${SiteDistribution.DomainName}/index.html"
@@ -50,7 +49,7 @@ Resources:
         DependsOn: SiteDistribution
 
   Rest:
-    Type: !Rain::Module "../../modules/rest-api.yaml"
+    Source: ../../modules/rest-api.yaml
     Properties:
       AppName: !Ref AppName
       UserPoolArn: !GetAtt CognitoUserPool.Arn
@@ -65,15 +64,15 @@ Resources:
           - JwtResourceOptions
 
   TestResource:
-    Type: !Rain::Module "../../modules/api-resource.yaml"
+    Source: ../../modules/api-resource.yaml
     Metadata:
       Comment: This module handles all methods on the /test path on the API. The lambda function code is located in api/resources/test.  
     Properties:
       Name: !Sub ${AppName}-test
       RestApi: !Ref RestApi
       RestApiDeployment: !Ref RestApiDeployment
-      BuildScript: ../test/webapp/buildapi.sh
-      CodePath: ../test/webapp/api/dist/test/lambda-handler.zip
+      BuildScript: buildapi.sh
+      CodePath: api/dist/test/lambda-handler.zip
       ResourcePath: test
       StageName: staging
       AuthorizerId: !Ref RestApiAuthorizer
@@ -85,21 +84,21 @@ Resources:
               TABLE_NAME: !Ref TestDataTable
 
   TestData:
-    Type: !Rain::Module "../../modules/simple-table.yaml" 
+    Source: ../../modules/simple-table.yaml
     Properties:
       TableName: !Sub ${AppName}-test
       LambdaRole: !Ref TestResourceHandlerRole
 
   JwtResource:
-    Type: !Rain::Module "../../modules/api-resource.yaml"
+    Source: ../../modules/api-resource.yaml
     Metadata:
       Comment: This module handles all methods on the /jwt path on the API. The lambda function code is located in api/resources/jwt
     Properties:
       Name: !Sub ${AppName}-jwt
       RestApi: !Ref RestApi
       RestApiDeployment: !Ref RestApiDeployment
-      BuildScript: ../test/webapp/buildapi.sh
-      CodePath: ../test/webapp/api/dist/jwt/lambda-handler.zip
+      BuildScript: buildapi.sh
+      CodePath: api/dist/jwt/lambda-handler.zip
       ResourcePath: jwt
       StageName: staging
       AuthorizerId: AWS::NoValue
@@ -136,4 +135,3 @@ Outputs:
 
   CognitoDomainPrefix:
     Value: !Ref AppName
-