Skip to content

Commit 6761b76

Browse files
committed
fixup! refactor: Move merge function to common module
1 parent ba5ba73 commit 6761b76

File tree

6 files changed

+278
-252
lines changed

6 files changed

+278
-252
lines changed

api/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ require (
2525
)
2626

2727
require (
28+
dario.cat/mergo v1.0.1 // indirect
2829
github.com/beorn7/perks v1.0.1 // indirect
2930
github.com/cespare/xxhash/v2 v2.3.0 // indirect
3031
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect

api/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
2+
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
13
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
24
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
35
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=

common/pkg/capi/clustertopology/handlers/mutation/meta.go

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@ package mutation
55

66
import (
77
"context"
8-
"encoding/json"
98
"fmt"
10-
"maps"
119
"sync"
1210

13-
"dario.cat/mergo"
1411
"github.com/samber/lo"
1512
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1613
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -22,6 +19,7 @@ import (
2219
"sigs.k8s.io/controller-runtime/pkg/client"
2320

2421
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers"
22+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/variables"
2523
)
2624

2725
type MutateFunc func(
@@ -130,7 +128,7 @@ func (mgp metaGeneratePatches) GeneratePatches(
130128
// Merge the global variables to the current resource vars. This allows the handlers to access
131129
// the variables defined in the cluster class or cluster configuration and use these correctly when
132130
// overrides are specified on machine deployment or control plane configuration.
133-
mergedVars, err := mergeVariableOverridesWithGlobal(vars, globalVars)
131+
mergedVars, err := variables.MergeVariableOverridesWithGlobal(vars, globalVars)
134132
if err != nil {
135133
log.Error(err, "Failed to merge global variables")
136134
return err
@@ -161,56 +159,3 @@ func (mgp metaGeneratePatches) GeneratePatches(
161159
},
162160
)
163161
}
164-
165-
// mergeVariableOverridesWithGlobal merges the provided variable overrides with the global variables.
166-
// It performs a deep merge, ensuring that if a variable exists in both maps, the value from the overrides is used.
167-
func mergeVariableOverridesWithGlobal(
168-
overrideVars, globalVars map[string]apiextensionsv1.JSON,
169-
) (map[string]apiextensionsv1.JSON, error) {
170-
mergedVars := maps.Clone(overrideVars)
171-
172-
for k, v := range globalVars {
173-
// If the value of v is nil, skip it.
174-
if v.Raw == nil {
175-
continue
176-
}
177-
178-
existingValue, exists := mergedVars[k]
179-
180-
// If the variable does not exist in the mergedVars or the value is nil, add it and continue.
181-
if !exists || existingValue.Raw == nil {
182-
mergedVars[k] = v
183-
continue
184-
}
185-
186-
// Wrap the value in a temporary key to ensure we can unmarshal to a map.
187-
// This is necessary because the values could be scalars.
188-
tempValJSON := fmt.Sprintf(`{"value": %s}`, string(existingValue.Raw))
189-
tempGlobalValJSON := fmt.Sprintf(`{"value": %s}`, string(v.Raw))
190-
191-
// Unmarshal the existing value and the global value into maps.
192-
var val, globalVal map[string]interface{}
193-
if err := json.Unmarshal([]byte(tempValJSON), &val); err != nil {
194-
return nil, fmt.Errorf("failed to unmarshal existing value for key %q: %w", k, err)
195-
}
196-
197-
if err := json.Unmarshal([]byte(tempGlobalValJSON), &globalVal); err != nil {
198-
return nil, fmt.Errorf("failed to unmarshal global value for key %q: %w", k, err)
199-
}
200-
201-
// Now use mergo to perform a deep merge of the values, retaining the values in `val` if present.
202-
if err := mergo.Merge(&val, globalVal); err != nil {
203-
return nil, fmt.Errorf("failed to merge values for key %q: %w", k, err)
204-
}
205-
206-
// Marshal the merged value back to JSON.
207-
mergedVal, err := json.Marshal(val["value"])
208-
if err != nil {
209-
return nil, fmt.Errorf("failed to marshal merged value for key %q: %w", k, err)
210-
}
211-
212-
mergedVars[k] = apiextensionsv1.JSON{Raw: mergedVal}
213-
}
214-
215-
return mergedVars, nil
216-
}

common/pkg/capi/clustertopology/handlers/mutation/meta_test.go

Lines changed: 0 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -289,198 +289,3 @@ func jsonPatch(operations ...jsonpatch.Operation) []byte {
289289
)
290290
return b
291291
}
292-
293-
func TestMergeVariableDefinitions(t *testing.T) {
294-
t.Parallel()
295-
296-
type args struct {
297-
vars map[string]apiextensionsv1.JSON
298-
globalVars map[string]apiextensionsv1.JSON
299-
}
300-
tests := []struct {
301-
name string
302-
args args
303-
want map[string]apiextensionsv1.JSON
304-
wantErr bool
305-
errString string
306-
}{
307-
{
308-
name: "no overlap, globalVars added",
309-
args: args{
310-
vars: map[string]apiextensionsv1.JSON{
311-
"a": {Raw: []byte(`1`)},
312-
},
313-
globalVars: map[string]apiextensionsv1.JSON{
314-
"b": {Raw: []byte(`2`)},
315-
},
316-
},
317-
want: map[string]apiextensionsv1.JSON{
318-
"a": {Raw: []byte(`1`)},
319-
"b": {Raw: []byte(`2`)},
320-
},
321-
},
322-
{
323-
name: "globalVars value is nil, skipped",
324-
args: args{
325-
vars: map[string]apiextensionsv1.JSON{
326-
"a": {Raw: []byte(`1`)},
327-
},
328-
globalVars: map[string]apiextensionsv1.JSON{
329-
"b": {Raw: nil},
330-
},
331-
},
332-
want: map[string]apiextensionsv1.JSON{
333-
"a": {Raw: []byte(`1`)},
334-
},
335-
},
336-
{
337-
name: "existing value is nil, globalVars value used",
338-
args: args{
339-
vars: map[string]apiextensionsv1.JSON{
340-
"a": {Raw: nil},
341-
},
342-
globalVars: map[string]apiextensionsv1.JSON{
343-
"a": {Raw: []byte(`2`)},
344-
},
345-
},
346-
want: map[string]apiextensionsv1.JSON{
347-
"a": {Raw: []byte(`2`)},
348-
},
349-
},
350-
{
351-
name: "both values are scalars, globalVars ignored",
352-
args: args{
353-
vars: map[string]apiextensionsv1.JSON{
354-
"a": {Raw: []byte(`1`)},
355-
},
356-
globalVars: map[string]apiextensionsv1.JSON{
357-
"a": {Raw: []byte(`2`)},
358-
},
359-
},
360-
want: map[string]apiextensionsv1.JSON{
361-
"a": {Raw: []byte(`1`)},
362-
},
363-
},
364-
{
365-
name: "both values are objects, merged",
366-
args: args{
367-
vars: map[string]apiextensionsv1.JSON{
368-
"a": {Raw: []byte(`{"x":1,"y":2}`)},
369-
},
370-
globalVars: map[string]apiextensionsv1.JSON{
371-
"a": {Raw: []byte(`{"y":3,"z":4}`)},
372-
},
373-
},
374-
want: map[string]apiextensionsv1.JSON{
375-
"a": {Raw: []byte(`{"x":1,"y":2,"z":4}`)},
376-
},
377-
},
378-
{
379-
name: "both values are objects with nested objects, merged",
380-
args: args{
381-
vars: map[string]apiextensionsv1.JSON{
382-
"a": {Raw: []byte(`{"x":1,"y":{"a": 2,"b":{"c": 3}}}`)},
383-
},
384-
globalVars: map[string]apiextensionsv1.JSON{
385-
"a": {Raw: []byte(`{"y":{"a": 2,"b":{"c": 5, "d": 6}},"z":4}`)},
386-
},
387-
},
388-
want: map[string]apiextensionsv1.JSON{
389-
"a": {Raw: []byte(`{"x":1,"y":{"a": 2,"b":{"c": 3, "d": 6}},"z":4}`)},
390-
},
391-
},
392-
{
393-
name: "both values are objects with nested objects with vars having nil object explicitly, merged",
394-
args: args{
395-
vars: map[string]apiextensionsv1.JSON{
396-
"a": {Raw: []byte(`{"x":1,"y":{"a": 2,"b": null}}`)},
397-
},
398-
globalVars: map[string]apiextensionsv1.JSON{
399-
"a": {Raw: []byte(`{"y":{"a": 2,"b":{"c": 5, "d": 6}},"z":4}`)},
400-
},
401-
},
402-
want: map[string]apiextensionsv1.JSON{
403-
"a": {Raw: []byte(`{"x":1,"y":{"a": 2,"b":{"c": 5, "d": 6}},"z":4}`)},
404-
},
405-
},
406-
{
407-
name: "globalVars is scalar, vars is object, keep object",
408-
args: args{
409-
vars: map[string]apiextensionsv1.JSON{
410-
"a": {Raw: []byte(`{"x":1}`)},
411-
},
412-
globalVars: map[string]apiextensionsv1.JSON{
413-
"a": {Raw: []byte(`2`)},
414-
},
415-
},
416-
want: map[string]apiextensionsv1.JSON{
417-
"a": {Raw: []byte(`{"x":1}`)},
418-
},
419-
},
420-
{
421-
name: "vars is scalar, globalVars is object, keep scalar",
422-
args: args{
423-
vars: map[string]apiextensionsv1.JSON{
424-
"a": {Raw: []byte(`2`)},
425-
},
426-
globalVars: map[string]apiextensionsv1.JSON{
427-
"a": {Raw: []byte(`{"x":1}`)},
428-
},
429-
},
430-
want: map[string]apiextensionsv1.JSON{
431-
"a": {Raw: []byte(`2`)},
432-
},
433-
},
434-
{
435-
name: "invalid JSON in vars",
436-
args: args{
437-
vars: map[string]apiextensionsv1.JSON{
438-
"a": {Raw: []byte(`{invalid}`)},
439-
},
440-
globalVars: map[string]apiextensionsv1.JSON{
441-
"a": {Raw: []byte(`{"x":1}`)},
442-
},
443-
},
444-
wantErr: true,
445-
errString: "failed to unmarshal existing value for key \"a\"",
446-
},
447-
{
448-
name: "invalid JSON in globalVars",
449-
args: args{
450-
vars: map[string]apiextensionsv1.JSON{
451-
"a": {Raw: []byte(`{"x":1}`)},
452-
},
453-
globalVars: map[string]apiextensionsv1.JSON{
454-
"a": {Raw: []byte(`{invalid}`)},
455-
},
456-
},
457-
wantErr: true,
458-
errString: "failed to unmarshal global value for key \"a\"",
459-
},
460-
}
461-
462-
for _, tt := range tests {
463-
t.Run(tt.name, func(t *testing.T) {
464-
t.Parallel()
465-
g := gomega.NewWithT(t)
466-
got, err := mergeVariableOverridesWithGlobal(tt.args.vars, tt.args.globalVars)
467-
if tt.wantErr {
468-
g.Expect(err).To(gomega.HaveOccurred())
469-
g.Expect(err.Error()).To(gomega.ContainSubstring(tt.errString))
470-
return
471-
}
472-
g.Expect(err).ToNot(gomega.HaveOccurred())
473-
// Compare JSON values
474-
for k, wantVal := range tt.want {
475-
gotVal, ok := got[k]
476-
g.Expect(ok).To(gomega.BeTrue(), "missing key %q", k)
477-
var wantObj, gotObj interface{}
478-
_ = json.Unmarshal(wantVal.Raw, &wantObj)
479-
_ = json.Unmarshal(gotVal.Raw, &gotObj)
480-
g.Expect(gotObj).To(gomega.Equal(wantObj), "key %q", k)
481-
}
482-
// Check for unexpected keys
483-
g.Expect(len(got)).To(gomega.Equal(len(tt.want)))
484-
})
485-
}
486-
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2023 Nutanix. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package variables
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"maps"
10+
11+
"dario.cat/mergo"
12+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
13+
)
14+
15+
// MergeVariableOverridesWithGlobal merges the provided variable overrides with the global variables.
16+
// It performs a deep merge, ensuring that if a variable exists in both maps, the value from the overrides is used.
17+
func MergeVariableOverridesWithGlobal(
18+
overrideVars, globalVars map[string]apiextensionsv1.JSON,
19+
) (map[string]apiextensionsv1.JSON, error) {
20+
mergedVars := maps.Clone(overrideVars)
21+
22+
for k, v := range globalVars {
23+
// If the value of v is nil, skip it.
24+
if v.Raw == nil {
25+
continue
26+
}
27+
28+
existingValue, exists := mergedVars[k]
29+
30+
// If the variable does not exist in the mergedVars or the value is nil, add it and continue.
31+
if !exists || existingValue.Raw == nil {
32+
mergedVars[k] = v
33+
continue
34+
}
35+
36+
// Wrap the value in a temporary key to ensure we can unmarshal to a map.
37+
// This is necessary because the values could be scalars.
38+
tempValJSON := fmt.Sprintf(`{"value": %s}`, string(existingValue.Raw))
39+
tempGlobalValJSON := fmt.Sprintf(`{"value": %s}`, string(v.Raw))
40+
41+
// Unmarshal the existing value and the global value into maps.
42+
var val, globalVal map[string]interface{}
43+
if err := json.Unmarshal([]byte(tempValJSON), &val); err != nil {
44+
return nil, fmt.Errorf("failed to unmarshal existing value for key %q: %w", k, err)
45+
}
46+
47+
if err := json.Unmarshal([]byte(tempGlobalValJSON), &globalVal); err != nil {
48+
return nil, fmt.Errorf("failed to unmarshal global value for key %q: %w", k, err)
49+
}
50+
51+
// Now use mergo to perform a deep merge of the values, retaining the values in `val` if present.
52+
if err := mergo.Merge(&val, globalVal); err != nil {
53+
return nil, fmt.Errorf("failed to merge values for key %q: %w", k, err)
54+
}
55+
56+
// Marshal the merged value back to JSON.
57+
mergedVal, err := json.Marshal(val["value"])
58+
if err != nil {
59+
return nil, fmt.Errorf("failed to marshal merged value for key %q: %w", k, err)
60+
}
61+
62+
mergedVars[k] = apiextensionsv1.JSON{Raw: mergedVal}
63+
}
64+
65+
return mergedVars, nil
66+
}

0 commit comments

Comments
 (0)