From 30720e65b7893f88fe82bb3f69ca378021168c97 Mon Sep 17 00:00:00 2001 From: Florian Stadler Date: Thu, 7 Nov 2024 17:56:23 +0100 Subject: [PATCH] Add utility functions for supporting CloudFormation CustomResources --- provider/pkg/naming/convert.go | 41 ++++++++ provider/pkg/naming/convert_test.go | 117 ++++++++++++++++++++++ provider/pkg/resources/checkpoint.go | 17 +++- provider/pkg/resources/checkpoint_test.go | 55 ++++++++++ 4 files changed, 227 insertions(+), 3 deletions(-) diff --git a/provider/pkg/naming/convert.go b/provider/pkg/naming/convert.go index 06d022c060..48f762c7fa 100644 --- a/provider/pkg/naming/convert.go +++ b/provider/pkg/naming/convert.go @@ -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{} diff --git a/provider/pkg/naming/convert_test.go b/provider/pkg/naming/convert_test.go index 74daa866cd..768525ac98 100644 --- a/provider/pkg/naming/convert_test.go +++ b/provider/pkg/naming/convert_test.go @@ -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) + }) + } +} + diff --git a/provider/pkg/resources/checkpoint.go b/provider/pkg/resources/checkpoint.go index 1145c28150..8bd71d3728 100644 --- a/provider/pkg/resources/checkpoint.go +++ b/provider/pkg/resources/checkpoint.go @@ -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. diff --git a/provider/pkg/resources/checkpoint_test.go b/provider/pkg/resources/checkpoint_test.go index abd9c33858..2ae7505148 100644 --- a/provider/pkg/resources/checkpoint_test.go +++ b/provider/pkg/resources/checkpoint_test.go @@ -8,6 +8,8 @@ import ( ) func TestCheckpointObject(t *testing.T) { + t.Parallel() + inputs := resource.PropertyMap{ "input1": resource.NewStringProperty("value1"), "input2": resource.NewNumberProperty(42), @@ -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), @@ -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), @@ -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) +}