Skip to content

Commit 65e1f62

Browse files
committed
Fix Terraform recipe parameter serialization issues
- Handle empty maps/arrays passed as strings in recipe parameters - Add recursive normalization for nested structures with string-encoded empty objects - Support whitespace variations (e.g., '{ }', '{}\n', '[ ]') - Remove TF_LOG environment variable to prevent terraform-exec conflicts - Add comprehensive test coverage for parameter normalization Signed-off-by: ytimocin <[email protected]>
1 parent 58e0b64 commit 65e1f62

File tree

6 files changed

+197
-1
lines changed

6 files changed

+197
-1
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,7 @@ hack/bicep-types-radius/generated/**/*.md
6969

7070
# Bicep extensions
7171
*.tgz
72+
73+
# Demo
74+
demo/gitlab/
75+
demo/recipes/

pkg/recipes/terraform/config/config_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,25 @@ func Test_NewConfig(t *testing.T) {
175175
},
176176
expectedConfigFile: "testdata/module-emptytemplateversion.tf.json",
177177
},
178+
{
179+
desc: "empty map as string parameter",
180+
moduleName: testRecipeName,
181+
envdef: &recipes.EnvironmentDefinition{
182+
Name: testRecipeName,
183+
TemplatePath: testTemplatePath,
184+
TemplateVersion: testTemplateVersion,
185+
Parameters: envParams,
186+
},
187+
metadata: &recipes.ResourceMetadata{
188+
Name: testRecipeName,
189+
Parameters: map[string]any{
190+
"redis_cache_name": "redis-test",
191+
"sku": "P",
192+
"tags": "{}", // This should be converted to an empty map
193+
},
194+
},
195+
expectedConfigFile: "testdata/module-emptymap.tf.json",
196+
},
178197
}
179198

180199
for _, tc := range configTests {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"terraform": null,
3+
"module": {
4+
"redis-azure": {
5+
"redis_cache_name": "redis-test",
6+
"resource_group_name": "test-rg",
7+
"sku": "P",
8+
"source": "Azure/redis/azurerm",
9+
"tags": {},
10+
"version": "1.1.0"
11+
}
12+
}
13+
}

pkg/recipes/terraform/config/types.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ limitations under the License.
1616

1717
package config
1818

19+
import "strings"
20+
1921
const (
2022
// moduleSourceKey represents the key for the module source parameter.
2123
moduleSourceKey = "source"
@@ -34,7 +36,50 @@ type RecipeParams map[string]any
3436
// SetParams sets the recipe parameters in the Terraform module configuration.
3537
func (tf TFModuleConfig) SetParams(params RecipeParams) {
3638
for k, v := range params {
37-
tf[k] = v
39+
tf[k] = normalizeValue(v)
40+
}
41+
}
42+
43+
// normalizeValue recursively normalizes parameter values, converting string representations
44+
// of empty objects/arrays to their proper types
45+
func normalizeValue(v any) any {
46+
switch val := v.(type) {
47+
case string:
48+
// Trim outer whitespace for comparison
49+
trimmed := strings.TrimSpace(val)
50+
51+
// Handle empty object representations (including with spaces inside)
52+
if trimmed == "{}" || trimmed == "{ }" {
53+
return map[string]any{}
54+
}
55+
56+
// Handle empty array representations (including with spaces inside)
57+
if trimmed == "[]" || trimmed == "[ ]" {
58+
return []any{}
59+
}
60+
61+
// Return the original string if no conversion needed
62+
return v
63+
64+
case map[string]any:
65+
// Recursively normalize nested maps
66+
result := make(map[string]any)
67+
for k, v := range val {
68+
result[k] = normalizeValue(v)
69+
}
70+
return result
71+
72+
case []any:
73+
// Recursively normalize arrays
74+
result := make([]any, len(val))
75+
for i, item := range val {
76+
result[i] = normalizeValue(item)
77+
}
78+
return result
79+
80+
default:
81+
// Return unchanged for other types
82+
return v
3883
}
3984
}
4085

pkg/recipes/terraform/config/types_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,114 @@ func TestSetParams(t *testing.T) {
3838
require.Equal(t, c["foo"].(map[string]any), map[string]any{"bar": "baz"})
3939
require.Equal(t, c["bar"].(map[string]any), map[string]any{"baz": "foo"})
4040
}
41+
42+
func TestSetParams_EmptyMapString(t *testing.T) {
43+
c := TFModuleConfig{}
44+
45+
c.SetParams(RecipeParams{
46+
"tags": "{}", // This should be converted to an empty map
47+
"normalParam": "value",
48+
"existingMap": map[string]any{
49+
"key": "value",
50+
},
51+
"emptyMap": map[string]any{}, // This should remain as is
52+
})
53+
54+
require.Equal(t, 4, len(c))
55+
56+
// Verify that the string "{}" was converted to an empty map
57+
tags, ok := c["tags"].(map[string]any)
58+
require.True(t, ok, "tags should be a map[string]any")
59+
require.Equal(t, map[string]any{}, tags)
60+
61+
// Verify other parameters remain unchanged
62+
require.Equal(t, "value", c["normalParam"])
63+
require.Equal(t, map[string]any{"key": "value"}, c["existingMap"])
64+
require.Equal(t, map[string]any{}, c["emptyMap"])
65+
}
66+
67+
func TestSetParams_WhitespaceHandling(t *testing.T) {
68+
c := TFModuleConfig{}
69+
70+
c.SetParams(RecipeParams{
71+
"tags1": "{ }", // With space inside
72+
"tags2": "{}\n", // With newline
73+
"tags3": " {} ", // With surrounding spaces
74+
"array1": "[]", // Empty array
75+
"array2": "[ ]", // Array with space inside
76+
"array3": "[]\n", // Array with newline
77+
})
78+
79+
// Verify empty maps (all variations should be converted)
80+
for _, key := range []string{"tags1", "tags2", "tags3"} {
81+
val, ok := c[key].(map[string]any)
82+
require.True(t, ok, "%s should be a map[string]any", key)
83+
require.Equal(t, map[string]any{}, val, "%s should be an empty map", key)
84+
}
85+
86+
// Verify empty arrays (all variations should be converted)
87+
for _, key := range []string{"array1", "array2", "array3"} {
88+
val, ok := c[key].([]any)
89+
require.True(t, ok, "%s should be a []any", key)
90+
require.Equal(t, []any{}, val, "%s should be an empty array", key)
91+
}
92+
}
93+
94+
func TestSetParams_NestedStructures(t *testing.T) {
95+
c := TFModuleConfig{}
96+
97+
c.SetParams(RecipeParams{
98+
"config": map[string]any{
99+
"tags": "{}",
100+
"metadata": "{}",
101+
"items": "[]",
102+
"nested": map[string]any{
103+
"deep": map[string]any{
104+
"tags": "{}",
105+
},
106+
},
107+
},
108+
"list": []any{
109+
"{}",
110+
"[]",
111+
map[string]any{
112+
"tags": "{}",
113+
},
114+
},
115+
})
116+
117+
// Verify nested map normalization
118+
config := c["config"].(map[string]any)
119+
require.Equal(t, map[string]any{}, config["tags"])
120+
require.Equal(t, map[string]any{}, config["metadata"])
121+
require.Equal(t, []any{}, config["items"])
122+
123+
// Verify deep nested normalization
124+
nested := config["nested"].(map[string]any)
125+
deep := nested["deep"].(map[string]any)
126+
require.Equal(t, map[string]any{}, deep["tags"])
127+
128+
// Verify array normalization
129+
list := c["list"].([]any)
130+
require.Equal(t, map[string]any{}, list[0])
131+
require.Equal(t, []any{}, list[1])
132+
nestedInList := list[2].(map[string]any)
133+
require.Equal(t, map[string]any{}, nestedInList["tags"])
134+
}
135+
136+
func TestSetParams_PreservesNonEmptyStrings(t *testing.T) {
137+
c := TFModuleConfig{}
138+
139+
c.SetParams(RecipeParams{
140+
"notEmpty1": "{\"key\": \"value\"}",
141+
"notEmpty2": "[1, 2, 3]",
142+
"notEmpty3": "{ key: value }",
143+
"normalStr": "just a string",
144+
})
145+
146+
// All these should remain as strings since they're not empty
147+
require.Equal(t, "{\"key\": \"value\"}", c["notEmpty1"])
148+
require.Equal(t, "[1, 2, 3]", c["notEmpty2"])
149+
require.Equal(t, "{ key: value }", c["notEmpty3"])
150+
require.Equal(t, "just a string", c["normalStr"])
151+
}

pkg/recipes/terraform/execute.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ func (e *executor) setEnvironmentVariables(ctx context.Context, tf *tfexec.Terra
255255

256256
// Populate envVars with the environment variables from current process
257257
envVars := splitEnvVar(os.Environ())
258+
259+
// Remove TF_LOG to prevent conflict with terraform-exec's logging configuration
260+
delete(envVars, "TF_LOG")
261+
258262
var envVarUpdate bool
259263

260264
// Handle recipe config if present

0 commit comments

Comments
 (0)