Skip to content

Commit 8af941a

Browse files
committed
fix: Correctly handle variable overrides
Currently the metamutator only passes the override values when they are present on a machine deployment or control plane config. This commit fixes that by merging the global variables into the overrides, resulting in the behaviour that we always expected. As overrides are not that commonly used yet, we have not seen this issue but as taints, etc become more commonly used this will become an issue. Tests added to prove the fix as well.
1 parent bab9a86 commit 8af941a

File tree

4 files changed

+348
-13
lines changed

4 files changed

+348
-13
lines changed

common/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ go 1.23.0
88
toolchain go1.24.5
99

1010
require (
11+
dario.cat/mergo v1.0.1
1112
github.com/evanphx/json-patch/v5 v5.9.11
1213
github.com/go-logr/logr v1.4.3
1314
github.com/onsi/ginkgo/v2 v2.23.4

common/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
22
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
3+
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
4+
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
35
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
46
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
57
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=

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

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ package mutation
55

66
import (
77
"context"
8+
"encoding/json"
89
"fmt"
10+
"maps"
911
"sync"
1012

13+
"dario.cat/mergo"
14+
"github.com/samber/lo"
1115
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1216
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1317
"k8s.io/apimachinery/pkg/runtime"
@@ -89,6 +93,21 @@ func (mgp metaGeneratePatches) GeneratePatches(
8993
) {
9094
clusterKey := handlers.ClusterKeyFromReq(req)
9195
clusterGetter := mgp.CreateClusterGetter(clusterKey)
96+
97+
// Create a map of variables from the request that can be overridden by machine deployment or control plane
98+
// configuration.
99+
// Filter out the "builtin" variable, which is already present in the vars map and should not be overridden by
100+
// the global variables.
101+
globalVars := lo.FilterSliceToMap(
102+
req.Variables,
103+
func(v runtimehooksv1.Variable) (string, apiextensionsv1.JSON, bool) {
104+
if v.Name == "builtin" {
105+
return "", apiextensionsv1.JSON{}, false
106+
}
107+
return v.Name, v.Value, true
108+
},
109+
)
110+
92111
topologymutation.WalkTemplates(
93112
ctx,
94113
unstructured.UnstructuredJSONScheme,
@@ -108,20 +127,88 @@ func (mgp metaGeneratePatches) GeneratePatches(
108127

109128
log.V(3).Info("Starting mutation pipeline", "handlerCount", len(mgp.mutators))
110129

111-
for i, h := range mgp.mutators {
112-
handlerName := fmt.Sprintf("%T", h)
113-
log.V(5).Info("Running mutator", "index", i, "handler", handlerName)
130+
// Merge the global variables to the current resource vars. This allows the handlers to access
131+
// the variables defined in the cluster class or cluster configuration and use these correctly when
132+
// overrides are specified on machine deployment or control plane configuration.
133+
mergedVars, err := mergeVariableDefinitions(vars, globalVars)
134+
if err != nil {
135+
log.Error(err, "Failed to merge global variables")
136+
return err
137+
}
114138

115-
if err := h.Mutate(ctx, obj.(*unstructured.Unstructured), vars, holderRef, clusterKey, clusterGetter); err != nil {
116-
log.Error(err, "Mutator failed", "index", i, "handler", handlerName)
139+
for i, h := range mgp.mutators {
140+
mutatorType := fmt.Sprintf("%T", h)
141+
log.V(5).
142+
Info("Running mutator", "index", i, "handler", mutatorType, "vars", vars)
143+
144+
if err := h.Mutate(
145+
ctx,
146+
obj.(*unstructured.Unstructured),
147+
mergedVars,
148+
holderRef,
149+
clusterKey,
150+
clusterGetter,
151+
); err != nil {
152+
log.Error(err, "Mutator failed", "index", i, "handler", mutatorType)
117153
return err
118154
}
119155

120-
log.V(5).Info("Mutator completed successfully", "index", i, "handler", handlerName)
156+
log.V(5).Info("Mutator completed successfully", "index", i, "handler", mutatorType)
121157
}
122158

123159
log.V(3).Info("Mutation pipeline completed successfully", "handlerCount", len(mgp.mutators))
124160
return nil
125161
},
126162
)
127163
}
164+
165+
func mergeVariableDefinitions(
166+
vars, globalVars map[string]apiextensionsv1.JSON,
167+
) (map[string]apiextensionsv1.JSON, error) {
168+
mergedVars := maps.Clone(vars)
169+
170+
for k, v := range globalVars {
171+
// If the value of v is nil, skip it.
172+
if v.Raw == nil {
173+
continue
174+
}
175+
176+
existingValue, exists := mergedVars[k]
177+
178+
// If the variable does not exist in the mergedVars or the value is nil, add it and continue.
179+
if !exists || existingValue.Raw == nil {
180+
mergedVars[k] = v
181+
continue
182+
}
183+
184+
// Wrap the value in a temporary key to ensure we can unmarshal to a map.
185+
// This is necessary because the values could be scalars.
186+
tempValJSON := fmt.Sprintf(`{"value": %s}`, string(existingValue.Raw))
187+
tempGlobalValJSON := fmt.Sprintf(`{"value": %s}`, string(v.Raw))
188+
189+
// Unmarshal the existing value and the global value into maps.
190+
var val, globalVal map[string]interface{}
191+
if err := json.Unmarshal([]byte(tempValJSON), &val); err != nil {
192+
return nil, fmt.Errorf("failed to unmarshal existing value for key %q: %w", k, err)
193+
}
194+
195+
if err := json.Unmarshal([]byte(tempGlobalValJSON), &globalVal); err != nil {
196+
return nil, fmt.Errorf("failed to unmarshal global value for key %q: %w", k, err)
197+
}
198+
199+
// Now use mergo to perform a deep merge of the values, retaining the values in `val` if present.
200+
if err := mergo.Merge(&val, globalVal); err != nil {
201+
return nil, fmt.Errorf("failed to merge values for key %q: %w", k, err)
202+
}
203+
204+
// Marshal the merged value back to JSON.
205+
mergedVal, err := json.Marshal(val["value"])
206+
if err != nil {
207+
return nil, fmt.Errorf("failed to marshal merged value for key %q: %w", k, err)
208+
}
209+
210+
mergedVars[k] = apiextensionsv1.JSON{Raw: mergedVal}
211+
}
212+
213+
return mergedVars, nil
214+
}

0 commit comments

Comments
 (0)