Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utility functions for supporting CloudFormation CustomResources #1808

Merged
merged 2 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions provider/pkg/naming/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,45 @@ func cfnValueToSdk(value interface{}) interface{} {
}
}

// ToStringifiedMap creates a copy of the input map with all deeply nested primitive values converted to strings.
func ToStringifiedMap[K comparable](value map[K]interface{}) map[K]interface{} {
if value == nil {
return nil
}

result := map[K]interface{}{}
for k, v := range value {
result[k] = primitivesToString(v)
}
return result
}

func primitivesToString(value interface{}) interface{} {
if value == nil {
return nil
}

switch reflect.TypeOf(value).Kind() {
case reflect.Map:
mapValue := reflect.MakeMap(reflect.TypeOf(value))
iter := reflect.ValueOf(value).MapRange()
for iter.Next() {
mapValue.SetMapIndex(iter.Key(), reflect.ValueOf(primitivesToString(iter.Value().Interface())))
}

return mapValue.Interface()
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(value)
result := make([]interface{}, s.Len())
for i := 0; i < s.Len(); i++ {
result[i] = primitivesToString(s.Index(i).Interface())
}
return result
default:
return fmt.Sprintf("%v", value)
}
}

type ConversionError struct {
Type string
Value interface{}
Expand Down
152 changes: 152 additions & 0 deletions provider/pkg/naming/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,155 @@ func TestSanitizeCfnString(t *testing.T) {
})
}
}

func TestToStringifiedMap(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input map[string]interface{}
expected map[string]interface{}
}{
{
name: "Nil input",
input: nil,
expected: nil,
},
{
name: "Empty map",
input: map[string]interface{}{},
expected: map[string]interface{}{},
},
{
name: "Map with primitive values",
input: map[string]interface{}{
"string": "value",
"int": 42,
"float": 3.14,
"bool": true,
},
expected: map[string]interface{}{
"string": "value",
"int": "42",
"float": "3.14",
"bool": "true",
},
},
{
name: "Map with nested map",
input: map[string]interface{}{
"nested": map[string]interface{}{
"key": "value",
"num": 123,
},
},
expected: map[string]interface{}{
"nested": map[string]interface{}{
"key": "value",
"num": "123",
},
},
},
{
name: "Map with array",
input: map[string]interface{}{
"array": []interface{}{"a", 1, 2.5, false},
},
expected: map[string]interface{}{
"array": []interface{}{"a", "1", "2.5", "false"},
},
},
{
name: "Map with mixed nested structures",
input: map[string]interface{}{
"level1": map[string]interface{}{
"level2": []interface{}{
map[string]interface{}{
"key1": "value1",
"key2": 2,
},
3.14,
"string",
},
"anotherKey": true,
"arrayOfMaps": []interface{}{
map[string]interface{}{
"key1": "value1",
"key2": 2,
},
map[string]interface{}{
"key3": "value3",
"key4": 4,
},
},
},
},
expected: map[string]interface{}{
"level1": map[string]interface{}{
"level2": []interface{}{
map[string]interface{}{
"key1": "value1",
"key2": "2",
},
"3.14",
"string",
},
"anotherKey": "true",
"arrayOfMaps": []interface{}{
map[string]interface{}{
"key1": "value1",
"key2": "2",
},
map[string]interface{}{
"key3": "value3",
"key4": "4",
},
},
},
},
},
{
name: "Map with arbitrary keys and deeply nested structures",
input: map[string]interface{}{
"level1": map[interface{}]interface{}{
123: "numberKey",
true: map[string]interface{}{
"nestedKey": []interface{}{
map[string]interface{}{
"key1": "value1",
"key2": 2,
},
3.14,
"string",
},
},
"anotherKey": false,
},
},
expected: map[string]interface{}{
"level1": map[interface{}]interface{}{
123: "numberKey",
true: map[string]interface{}{
"nestedKey": []interface{}{
map[string]interface{}{
"key1": "value1",
"key2": "2",
},
"3.14",
"string",
},
},
"anotherKey": "false",
},
},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := ToStringifiedMap(tt.input)
assert.Equal(t, tt.expected, actual)
})
}
}
17 changes: 14 additions & 3 deletions provider/pkg/resources/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ import (

// CheckpointObject puts inputs in the `__inputs` field of the state.
func CheckpointObject(inputs resource.PropertyMap, outputs map[string]interface{}) resource.PropertyMap {
object := resource.NewPropertyMapFromMap(outputs)
object["__inputs"] = resource.MakeSecret(resource.NewObjectProperty(inputs))
return object
return CheckpointPropertyMap(inputs, resource.NewPropertyMapFromMap(outputs))
}

// CheckpointPropertyMap puts inputs in the `__inputs` field of the state.
func CheckpointPropertyMap(inputs resource.PropertyMap, outputs resource.PropertyMap) resource.PropertyMap {
var props resource.PropertyMap
if outputs == nil {
props = resource.PropertyMap{}
} else {
props = outputs.Copy()
}

props["__inputs"] = resource.MakeSecret(resource.NewObjectProperty(inputs))
return props
}

// ParseCheckpointObject returns inputs that are saved in the `__inputs` field of the state.
Expand Down
55 changes: 55 additions & 0 deletions provider/pkg/resources/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
)

func TestCheckpointObject(t *testing.T) {
t.Parallel()

inputs := resource.PropertyMap{
"input1": resource.NewStringProperty("value1"),
"input2": resource.NewNumberProperty(42),
Expand Down Expand Up @@ -35,6 +37,8 @@ func TestCheckpointObject(t *testing.T) {
}

func TestParseCheckpointObject(t *testing.T) {
t.Parallel()

inputs := resource.PropertyMap{
"input1": resource.NewStringProperty("value1"),
"input2": resource.NewNumberProperty(42),
Expand Down Expand Up @@ -65,6 +69,8 @@ func TestParseCheckpointObject(t *testing.T) {
}

func TestRoundTripCheckpointObject(t *testing.T) {
t.Parallel()

inputs := resource.PropertyMap{
"input1": resource.NewStringProperty("value1"),
"input2": resource.NewNumberProperty(42),
Expand All @@ -88,3 +94,52 @@ func TestRoundTripCheckpointObject(t *testing.T) {
assert.Equal(t, resource.NewStringProperty("value1"), checkpoint["output1"])
assert.Equal(t, resource.NewNumberProperty(42), checkpoint["output2"])
}

func TestCheckpointPropertyMap(t *testing.T) {
t.Parallel()

inputs := resource.PropertyMap{
"input1": resource.NewStringProperty("value1"),
"input2": resource.NewNumberProperty(42),
}

outputs := resource.PropertyMap{
"output1": resource.NewStringProperty("value1"),
"output2": resource.NewNumberProperty(42),
}

result := CheckpointPropertyMap(inputs, outputs)

// Check if outputs are correctly set
assert.Equal(t, resource.NewStringProperty("value1"), result["output1"])
assert.Equal(t, resource.NewNumberProperty(42), result["output2"])

// Check if __inputs field is correctly set and is a secret
inputsField, ok := result["__inputs"]
assert.True(t, ok)
assert.True(t, inputsField.IsSecret())

// Check if the secret value contains the correct inputs
secretInputs := inputsField.SecretValue().Element.ObjectValue()
assert.Equal(t, inputs, secretInputs)
}

func TestCheckpointPropertyMapWithNilOutputs(t *testing.T) {
t.Parallel()

inputs := resource.PropertyMap{
"input1": resource.NewStringProperty("value1"),
"input2": resource.NewNumberProperty(42),
}

result := CheckpointPropertyMap(inputs, nil)

// Check if __inputs field is correctly set and is a secret
inputsField, ok := result["__inputs"]
assert.True(t, ok)
assert.True(t, inputsField.IsSecret())

// Check if the secret value contains the correct inputs
secretInputs := inputsField.SecretValue().Element.ObjectValue()
assert.Equal(t, inputs, secretInputs)
}
Loading