Skip to content

Commit

Permalink
Merge pull request #465 from turkenh/fix-conversion
Browse files Browse the repository at this point in the history
Expose conversion option to inject key/values in the conversion to list
  • Loading branch information
sergenyalcin authored Feb 6, 2025
2 parents ce71033 + 1637849 commit 1a6d69b
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 12 deletions.
28 changes: 22 additions & 6 deletions pkg/config/conversion/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
48 changes: 47 additions & 1 deletion pkg/config/conversion/conversions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ func TestSingletonListConversion(t *testing.T) {
targetMap map[string]any
crdPaths []string
mode ListConversionMode
opts []SingletonListConversionOption
}
type want struct {
converted bool
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 29 additions & 1 deletion pkg/config/conversion/list_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,25 @@ 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
// will be performed are specified using paths and the conversion mode (whether
// 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)
Expand All @@ -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)
}
Expand All @@ -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
}
144 changes: 143 additions & 1 deletion pkg/config/conversion/list_conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func TestConvert(t *testing.T) {
params map[string]any
paths []string
mode ListConversionMode
opts *ConvertOptions
}
type want struct {
err error
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/tf_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion pkg/examples/conversion/example_conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 1a6d69b

Please sign in to comment.