Repository: gruntwork-io/terragrunt (language: go)

Issue:
Terragrunt 0.53.6, S3 Remote State with `assume_role` throws error "Missing close bracket on index"
After upgrading to 0.53.6 and applying the new `assume_role` S3 configuration block, I get the following error:

```shell
 Error: Missing close bracket on index
│
│   on -backend-config="assume_role=map[role_arn:arn:aws:iam::111111111111:role/Administrator]" line 1:
│   (source code not available)
│
│ The index operator must end with a closing bracket ("]").
```

Here's my `terragrunt.hcl`:

```hcl
# terragrunt.hcl
remote_state {
  backend = "s3"
  config = {
    bucket                = "xxx"
    key                   = "${path_relative_to_include()}/terraform.tfstate"
    region                = "eu-central-1"
    # role_arn              = "arn:aws:iam::${get_env("TG_AWS_ACCT", "${get_aws_account_id()}")}:role/Administrator"
    assume_role = {
      role_arn            = "arn:aws:iam::${get_env("TG_AWS_ACCT", "${get_aws_account_id()}")}:role/Administrator"
    }
    encrypt               = true
    dynamodb_table        = "admin-terraform-lock"
    disable_bucket_update = true
    s3_bucket_tags = {
      owner = "terragrunt"
      name  = "Terraform state storage"
    }
    dynamodb_table_tags = {
      owner = "terragrunt"
      name  = "Terraform lock table"
    }
  }
}

terraform_version_constraint = "~>1.6.4"
terragrunt_version_constraint = "~>0.53.6"
```

Continuing to use `role_arn` as previously still works. I looked into the changes of #2800 but can't find one that would explain this error. I do assume that I'm missing something and will feel stupid as soon as someone points me to my obvious error 😅.

**Versions**
- Terragrunt version: 0.53.6 (installed via Homebrew)
- Terraform version: 1.6.4 (installed via Hashicorp's Homebrew Tap)
- MacOS Sonoma (14.1.1 (23B81))



Discussion / hints:
Hmm, looks like it is an issue with type conversion... will take a look soon

Fix patch:
diff --git a/remote/remote_state_s3.go b/remote/remote_state_s3.go
index f3bcc549f0..8fd9f30848 100644
--- a/remote/remote_state_s3.go
+++ b/remote/remote_state_s3.go
@@ -341,6 +341,7 @@ func (s3Initializer S3Initializer) GetTerraformInitArgs(config map[string]interf
 	const (
 		lockTableKey     = "lock_table"
 		dynamoDBTableKey = "dynamodb_table"
+		assumeRoleKey    = "assume_role"
 	)
 
 	for key, val := range config {
@@ -358,6 +359,12 @@ func (s3Initializer S3Initializer) GetTerraformInitArgs(config map[string]interf
 			filteredConfig[dynamoDBTableKey] = val
 			continue
 		}
+		if key == assumeRoleKey {
+			if mapVal, ok := val.(map[string]interface{}); ok {
+				filteredConfig[key] = wrapMapToSingleLineHcl(mapVal)
+				continue
+			}
+		}
 
 		filteredConfig[key] = val
 	}
diff --git a/remote/terraform_config.go b/remote/terraform_config.go
new file mode 100644
index 0000000000..f06b4ec99b
--- /dev/null
+++ b/remote/terraform_config.go
@@ -0,0 +1,30 @@
+package remote
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+)
+
+// wrapMapToSingleLineHcl - This is a workaround to convert a map[string]interface{} to a single line HCL string.
+func wrapMapToSingleLineHcl(m map[string]interface{}) string {
+	var attributes []string
+	for key, value := range m {
+		attributes = append(attributes, fmt.Sprintf(`%s=%s`, key, formatHclValue(value)))
+	}
+	sort.Strings(attributes)
+	return fmt.Sprintf("{%s}", strings.Join(attributes, ","))
+}
+
+// formatHclValue - Wrap single line HCL values in quotes.
+func formatHclValue(value interface{}) string {
+	switch v := value.(type) {
+	case string:
+		escapedValue := strings.ReplaceAll(v, `"`, `\"`)
+		return fmt.Sprintf(`"%s"`, escapedValue)
+	case map[string]interface{}:
+		return wrapMapToSingleLineHcl(v)
+	default:
+		return fmt.Sprintf(`%v`, v)
+	}
+}


Test patch:
diff --git a/remote/remote_state_s3_test.go b/remote/remote_state_s3_test.go
index 76ee0c8bcb..59fa341d3a 100644
--- a/remote/remote_state_s3_test.go
+++ b/remote/remote_state_s3_test.go
@@ -370,6 +370,22 @@ func TestGetTerraformInitArgs(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"assume-role",
+			map[string]interface{}{
+				"bucket": "foo",
+				"assume_role": map[string]interface{}{
+					"role_arn":     "arn:aws:iam::123:role/role",
+					"external_id":  "123",
+					"session_name": "qwe",
+				},
+			},
+			map[string]interface{}{
+				"bucket":      "foo",
+				"assume_role": "{external_id=\"123\",role_arn=\"arn:aws:iam::123:role/role\",session_name=\"qwe\"}",
+			},
+			true,
+		},
 	}
 
 	for _, testCase := range testCases {
diff --git a/remote/terraform_config_test.go b/remote/terraform_config_test.go
new file mode 100644
index 0000000000..9f04547bf7
--- /dev/null
+++ b/remote/terraform_config_test.go
@@ -0,0 +1,31 @@
+package remote
+
+import "testing"
+
+func TestWrapMapToSingleLineHcl(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    map[string]interface{}
+		expected string
+	}{
+		{
+			name:     "SimpleMap",
+			input:    map[string]interface{}{"key1": "value1", "key2": 46521694, "key3": true},
+			expected: `{key1="value1",key2=46521694,key3=true}`,
+		},
+		{
+			name:     "NestedMap",
+			input:    map[string]interface{}{"key1": "value1", "key2": map[string]interface{}{"nestedKey": "nestedValue"}},
+			expected: `{key1="value1",key2={nestedKey="nestedValue"}}`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := wrapMapToSingleLineHcl(tt.input)
+			if result != tt.expected {
+				t.Errorf("Expected %s, but got %s", tt.expected, result)
+			}
+		})
+	}
+}