Skip to content

Commit

Permalink
Add utility functions for supporting CloudFormation CustomResources
Browse files Browse the repository at this point in the history
  • Loading branch information
flostadler committed Nov 7, 2024
1 parent 6575dfa commit 30720e6
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 3 deletions.
41 changes: 41 additions & 0 deletions provider/pkg/naming/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,47 @@ func cfnValueToSdk(value interface{}) interface{} {
}
}

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

result := map[string]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:
valueMap, ok := value.(map[string]interface{})
if !ok {
return value
}
result := map[string]interface{}{}
for k, v := range valueMap {
result[k] = primitivesToString(v)
}
return result
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
117 changes: 117 additions & 0 deletions provider/pkg/naming/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,120 @@ 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",
},
},
},
},
},
}

for _, tt := range tests {
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)
}

0 comments on commit 30720e6

Please sign in to comment.