From 163784981f51f0175d7b4cf5cea06999395aa83a Mon Sep 17 00:00:00 2001 From: Hasan Turken Date: Wed, 5 Feb 2025 18:29:20 +0300 Subject: [PATCH] Expose conversion option to inject key/values in the conversion to list Signed-off-by: Hasan Turken --- pkg/config/conversion/conversions.go | 28 +++- pkg/config/conversion/conversions_test.go | 48 +++++- pkg/config/conversion/list_conversion.go | 30 +++- pkg/config/conversion/list_conversion_test.go | 144 +++++++++++++++++- pkg/config/tf_conversion.go | 4 +- .../conversion/example_conversions.go | 2 +- 6 files changed, 244 insertions(+), 12 deletions(-) diff --git a/pkg/config/conversion/conversions.go b/pkg/config/conversion/conversions.go index 016903ef..fe33a68e 100644 --- a/pkg/config/conversion/conversions.go +++ b/pkg/config/conversion/conversions.go @@ -178,22 +178,38 @@ func NewCustomConverter(sourceVersion, targetVersion string, converter func(src, type singletonListConverter struct { baseConversion - pathPrefixes []string - crdPaths []string - mode ListConversionMode + pathPrefixes []string + crdPaths []string + mode ListConversionMode + convertOptions *ConvertOptions +} + +type SingletonListConversionOption func(*singletonListConverter) + +// WithConvertOptions sets the ConvertOptions for the singleton list conversion. +func WithConvertOptions(opts *ConvertOptions) SingletonListConversionOption { + return func(s *singletonListConverter) { + s.convertOptions = opts + } } // NewSingletonListConversion returns a new Conversion from the specified // sourceVersion of an API to the specified targetVersion and uses the // CRD field paths given in crdPaths to convert between the singleton // lists and embedded objects in the given conversion mode. -func NewSingletonListConversion(sourceVersion, targetVersion string, pathPrefixes []string, crdPaths []string, mode ListConversionMode) Conversion { - return &singletonListConverter{ +func NewSingletonListConversion(sourceVersion, targetVersion string, pathPrefixes []string, crdPaths []string, mode ListConversionMode, opts ...SingletonListConversionOption) Conversion { + s := &singletonListConverter{ baseConversion: newBaseConversion(sourceVersion, targetVersion), pathPrefixes: pathPrefixes, crdPaths: crdPaths, mode: mode, } + + for _, o := range opts { + o(s) + } + + return s } func (s *singletonListConverter) ConvertPaved(src, target *fieldpath.Paved) (bool, error) { @@ -214,7 +230,7 @@ func (s *singletonListConverter) ConvertPaved(src, target *fieldpath.Paved) (boo if !ok { return true, errors.Errorf("value at path %s is not a map[string]any", p) } - if _, err := Convert(m, s.crdPaths, s.mode); err != nil { + if _, err := Convert(m, s.crdPaths, s.mode, s.convertOptions); err != nil { return true, errors.Wrapf(err, "failed to convert the source map in mode %q with %s", s.mode, s.baseConversion.String()) } if err := target.SetValue(p, m); err != nil { diff --git a/pkg/config/conversion/conversions_test.go b/pkg/config/conversion/conversions_test.go index c8fa4186..532b3ef5 100644 --- a/pkg/config/conversion/conversions_test.go +++ b/pkg/config/conversion/conversions_test.go @@ -366,6 +366,7 @@ func TestSingletonListConversion(t *testing.T) { targetMap map[string]any crdPaths []string mode ListConversionMode + opts []SingletonListConversionOption } type want struct { converted bool @@ -465,10 +466,55 @@ func TestSingletonListConversion(t *testing.T) { targetMap: map[string]any{}, }, }, + "SuccessfulToSingletonListConversionWithInjectedKey": { + reason: "Successful conversion from an embedded object to a singleton list.", + args: args{ + sourceVersion: AllVersions, + sourceMap: map[string]any{ + "spec": map[string]any{ + "initProvider": map[string]any{ + "o": map[string]any{ + "k": "v", + }, + }, + }, + }, + targetVersion: AllVersions, + targetMap: map[string]any{}, + crdPaths: []string{"o"}, + mode: ToSingletonList, + opts: []SingletonListConversionOption{ + WithConvertOptions(&ConvertOptions{ + ListInjectKeys: map[string]SingletonListInjectKey{ + "o": { + Key: "index", + Value: "0", + }, + }, + }), + }, + }, + want: want{ + converted: true, + targetMap: map[string]any{ + "spec": map[string]any{ + "initProvider": map[string]any{ + "o": []map[string]any{ + { + "k": "v", + "index": "0", + }, + }, + }, + }, + }, + }, + }, } for n, tc := range tests { t.Run(n, func(t *testing.T) { - c := NewSingletonListConversion(tc.args.sourceVersion, tc.args.targetVersion, []string{pathInitProvider}, tc.args.crdPaths, tc.args.mode) + + c := NewSingletonListConversion(tc.args.sourceVersion, tc.args.targetVersion, []string{pathInitProvider}, tc.args.crdPaths, tc.args.mode, tc.args.opts...) sourceMap, err := roundTrip(tc.args.sourceMap) if err != nil { t.Fatalf("Failed to preprocess tc.args.sourceMap: %v", err) diff --git a/pkg/config/conversion/list_conversion.go b/pkg/config/conversion/list_conversion.go index d7a1fb0f..c2a6d1a4 100644 --- a/pkg/config/conversion/list_conversion.go +++ b/pkg/config/conversion/list_conversion.go @@ -72,6 +72,17 @@ func setValue(pv *fieldpath.Paved, v any, fp string) error { return nil } +type SingletonListInjectKey struct { + Key string + Value string +} + +type ConvertOptions struct { + // ListInjectKeys is used to inject a key with a default value into the + // singleton list for a given path. + ListInjectKeys map[string]SingletonListInjectKey +} + // Convert performs conversion between singleton lists and embedded objects // while passing the CRD parameters to the Terraform layer and while reading // state from the Terraform layer at runtime. The paths where the conversion @@ -79,7 +90,7 @@ func setValue(pv *fieldpath.Paved, v any, fp string) error { // an embedded object will be converted into a singleton list or a singleton // list will be converted into an embedded object) is determined by the mode // parameter. -func Convert(params map[string]any, paths []string, mode ListConversionMode) (map[string]any, error) { //nolint:gocyclo // easier to follow as a unit +func Convert(params map[string]any, paths []string, mode ListConversionMode, opts *ConvertOptions) (map[string]any, error) { //nolint:gocyclo // easier to follow as a unit switch mode { case ToSingletonList: slices.Sort(paths) @@ -102,6 +113,15 @@ func Convert(params map[string]any, paths []string, mode ListConversionMode) (ma } switch mode { case ToSingletonList: + if opts != nil { + // We replace 0th index with "*" to be able to stay consistent + // with the paths parameter in the keys of opts.ListInjectKeys. + if inj, ok := opts.ListInjectKeys[strings.ReplaceAll(e, "0", "*")]; ok && inj.Key != "" && inj.Value != "" { + if m, ok := v.(map[string]any); ok { + m[inj.Key] = inj.Value + } + } + } if err := setValue(pv, []any{v}, e); err != nil { return nil, errors.Wrapf(err, "cannot set the singleton list's value at the field path %s", e) } @@ -121,11 +141,19 @@ func Convert(params map[string]any, paths []string, mode ListConversionMode) (ma newVal = s[0] } } + if opts != nil { + // We replace 0th index with "*" to be able to stay consistent + // with the paths parameter in the keys of opts.ListInjectKeys. + if inj, ok := opts.ListInjectKeys[strings.ReplaceAll(e, "0", "*")]; ok && inj.Key != "" && inj.Value != "" { + delete(newVal.(map[string]any), inj.Key) + } + } if err := setValue(pv, newVal, e); err != nil { return nil, errors.Wrapf(err, "cannot set the embedded object's value at the field path %s", e) } } } } + return params, nil } diff --git a/pkg/config/conversion/list_conversion_test.go b/pkg/config/conversion/list_conversion_test.go index 9308a17f..e1644e59 100644 --- a/pkg/config/conversion/list_conversion_test.go +++ b/pkg/config/conversion/list_conversion_test.go @@ -19,6 +19,7 @@ func TestConvert(t *testing.T) { params map[string]any paths []string mode ListConversionMode + opts *ConvertOptions } type want struct { err error @@ -269,6 +270,147 @@ func TestConvert(t *testing.T) { }, }, }, + "WithInjectedKeySingletonListToEmbeddedObject": { + reason: "Should successfully convert a singleton list at the root level to an embedded object.", + args: args{ + params: map[string]any{ + "l": []map[string]any{ + { + "k": "v", + "index": "0", + }, + }, + }, + paths: []string{"l"}, + mode: ToEmbeddedObject, + opts: &ConvertOptions{ + ListInjectKeys: map[string]SingletonListInjectKey{ + "l": { + Key: "index", + Value: "0", + }, + }, + }}, + want: want{ + params: map[string]any{ + "l": map[string]any{ + "k": "v", + }, + }, + }, + }, + "WithInjectedKeyEmbeddedObjectToSingletonList": { + reason: "Should successfully convert an embedded object at the root level to a singleton list.", + args: args{ + params: map[string]any{ + "l": map[string]any{ + "k": "v", + }, + }, + paths: []string{"l"}, + mode: ToSingletonList, + opts: &ConvertOptions{ + ListInjectKeys: map[string]SingletonListInjectKey{ + "l": { + Key: "index", + Value: "0", + }, + }, + }, + }, + want: want{ + params: map[string]any{ + "l": []map[string]any{ + { + "k": "v", + "index": "0", + }, + }, + }, + }, + }, + "WithInjectedKeyNestedEmbeddedObjectsToSingletonListInLexicalOrder": { + reason: "Should successfully convert the parent & nested embedded objects to singleton lists. Paths are specified in lexical order.", + args: args{ + params: map[string]any{ + "parent": map[string]any{ + "child": map[string]any{ + "k": "v", + }, + }, + }, + paths: []string{"parent", "parent[*].child"}, + mode: ToSingletonList, + opts: &ConvertOptions{ + ListInjectKeys: map[string]SingletonListInjectKey{ + "parent": { + Key: "index", + Value: "0", + }, + "parent[*].child": { + Key: "another", + Value: "0", + }, + }, + }, + }, + want: want{ + params: map[string]any{ + "parent": []map[string]any{ + { + "index": "0", + "child": []map[string]any{ + { + "k": "v", + "another": "0", + }, + }, + }, + }, + }, + }, + }, + "WithInjectedKeyNestedSingletonListsToEmbeddedObjectsPathsInLexicalOrder": { + reason: "Should successfully convert the parent & nested singleton lists to embedded objects. Paths specified in lexical order.", + args: args{ + params: map[string]any{ + "parent": []map[string]any{ + { + "index": "0", + "child": []map[string]any{ + { + "k": "v", + "another": "0", + }, + }, + }, + }, + }, + paths: []string{"parent", "parent[*].child"}, + mode: ToEmbeddedObject, + opts: &ConvertOptions{ + ListInjectKeys: map[string]SingletonListInjectKey{ + "parent": { + Key: "index", + Value: "0", + }, + "parent[*].child": { + Key: "another", + Value: "0", + }, + }, + }, + }, + want: want{ + params: map[string]any{ + "parent": map[string]any{ + "child": map[string]any{ + "k": "v", + }, + }, + }, + }, + }, } for n, tt := range tests { @@ -281,7 +423,7 @@ func TestConvert(t *testing.T) { if err != nil { t.Fatalf("Failed to preprocess tt.want.params: %v", err) } - got, err := Convert(params, tt.args.paths, tt.args.mode) + got, err := Convert(params, tt.args.paths, tt.args.mode, tt.args.opts) if diff := cmp.Diff(tt.want.err, err, test.EquateErrors()); diff != "" { t.Fatalf("\n%s\nConvert(tt.args.params, tt.args.paths): -wantErr, +gotErr:\n%s", tt.reason, diff) } diff --git a/pkg/config/tf_conversion.go b/pkg/config/tf_conversion.go index 71ff9514..7e0412fa 100644 --- a/pkg/config/tf_conversion.go +++ b/pkg/config/tf_conversion.go @@ -64,9 +64,9 @@ func (s singletonListConversion) Convert(params map[string]any, r *Resource, mod var m map[string]any switch mode { case FromTerraform: - m, err = conversion.Convert(params, r.TFListConversionPaths(), conversion.ToEmbeddedObject) + m, err = conversion.Convert(params, r.TFListConversionPaths(), conversion.ToEmbeddedObject, nil) case ToTerraform: - m, err = conversion.Convert(params, r.TFListConversionPaths(), conversion.ToSingletonList) + m, err = conversion.Convert(params, r.TFListConversionPaths(), conversion.ToSingletonList, nil) } return m, errors.Wrapf(err, "failed to convert between Crossplane and Terraform layers in mode %q", mode) } diff --git a/pkg/examples/conversion/example_conversions.go b/pkg/examples/conversion/example_conversions.go index aad1e0c2..a5d303d6 100644 --- a/pkg/examples/conversion/example_conversions.go +++ b/pkg/examples/conversion/example_conversions.go @@ -79,7 +79,7 @@ func ConvertSingletonListToEmbeddedObject(pc *config.Provider, startPath, licens // spec. conversionPaths[i] = "spec.forProvider." + cp } - converted, err := conversion.Convert(e.Object, conversionPaths, conversion.ToEmbeddedObject) + converted, err := conversion.Convert(e.Object, conversionPaths, conversion.ToEmbeddedObject, nil) if err != nil { return errors.Wrapf(err, "failed to convert example to embedded object in manifest %s", path) }