Skip to content

Commit d27476d

Browse files
authored
Merge pull request #8153 from killianmuldoon/pr-exvars-weaker-variable-validation-clusterclass-update
🌱 Weaken ClusterClass webhook variable validation on update
2 parents 59c6858 + 2b363e7 commit d27476d

File tree

2 files changed

+0
-1347
lines changed

2 files changed

+0
-1347
lines changed

internal/webhooks/clusterclass.go

Lines changed: 0 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package webhooks
1919
import (
2020
"context"
2121
"fmt"
22-
"reflect"
2322
"strings"
2423

2524
"github.com/pkg/errors"
@@ -176,10 +175,6 @@ func (webhook *ClusterClass) validate(ctx context.Context, oldClusterClass, newC
176175
// Ensure no MachineHealthCheck currently in use has been removed from the ClusterClass.
177176
allErrs = append(allErrs,
178177
validateUpdatesToMachineHealthCheckClasses(clusters, oldClusterClass, newClusterClass)...)
179-
180-
// Ensure no Variable would be invalidated by the update in spec
181-
allErrs = append(allErrs,
182-
validateVariableUpdates(clusters, oldClusterClass, newClusterClass, field.NewPath("spec", "variables"))...)
183178
}
184179

185180
if len(allErrs) > 0 {
@@ -188,118 +183,6 @@ func (webhook *ClusterClass) validate(ctx context.Context, oldClusterClass, newC
188183
return nil
189184
}
190185

191-
// validateVariableUpdates checks if the updates made to ClusterClassVariables are valid.
192-
// It retrieves a list of variables of interest and validates:
193-
// 1) Altered ClusterClassVariables defined on any exiting Cluster are still valid with the updated Schema.
194-
// 2) Removed ClusterClassVariables are not in use on any Cluster using the ClusterClass.
195-
// 3) Added ClusterClassVariables defined on any exiting Cluster are still valid with the updated Schema.
196-
// 4) Required ClusterClassVariables are defined on each Cluster using the ClusterClass.
197-
func validateVariableUpdates(clusters []clusterv1.Cluster, oldClusterClass, newClusterClass *clusterv1.ClusterClass, path *field.Path) field.ErrorList {
198-
tracker := map[string][]string{}
199-
200-
// Get the old ClusterClassVariables as a map
201-
oldVars, _ := getClusterClassVariablesMapWithReverseIndex(oldClusterClass.Spec.Variables)
202-
203-
// Get the new ClusterClassVariables as a map with an index linking them to their place in the ClusterClass Variable array.
204-
// Note: The index is used to improve the error recording below.
205-
newVars, clusterClassVariablesIndex := getClusterClassVariablesMapWithReverseIndex(newClusterClass.Spec.Variables)
206-
207-
// Compute the diff between old and new ClusterClassVariables.
208-
varsDiff := getClusterClassVariablesForValidation(oldVars, newVars)
209-
210-
errorInfo := errorAggregator{}
211-
allClusters := []string{}
212-
213-
// Validate the variable values on each Cluster ensuring they are still compatible with the new ClusterClass.
214-
for _, cluster := range clusters {
215-
allClusters = append(allClusters, cluster.Name)
216-
for _, c := range cluster.Spec.Topology.Variables {
217-
// copy variable to avoid memory aliasing.
218-
clusterVar := c
219-
220-
// Add Cluster Variable entry in clusterVariableReferences to track where it is in use.
221-
tracker[clusterVar.Name] = append(tracker[clusterVar.Name], cluster.Name)
222-
223-
// 1) Error if a variable with a schema altered in the update is no longer valid on the Cluster.
224-
if alteredVar, ok := varsDiff[variableValidationKey{clusterVar.Name, altered}]; ok {
225-
if errs := variables.ValidateClusterVariable(&clusterVar, alteredVar, field.NewPath("")); len(errs) > 0 {
226-
errorInfo.add(alteredVar.Name, altered, cluster.Name)
227-
}
228-
continue
229-
}
230-
231-
// 2) Error if a variable removed in the update is still in use in some Clusters.
232-
if _, ok := varsDiff[variableValidationKey{clusterVar.Name, removed}]; ok {
233-
errorInfo.add(clusterVar.Name, removed, cluster.Name)
234-
continue
235-
}
236-
237-
// 3) Error if a variable has been added in the update check is no longer valid on the Cluster.
238-
// NOTE: This can't occur in normal circumstances as a variable must be defined in a ClusterClass in order to be introduced in
239-
// a Cluster. This check may catch errors in cases involving broken Clusters.
240-
if addedVar, ok := varsDiff[variableValidationKey{clusterVar.Name, added}]; ok {
241-
if errs := variables.ValidateClusterVariable(&clusterVar, addedVar, field.NewPath("")); len(errs) > 0 {
242-
errorInfo.add(addedVar.Name, added, cluster.Name)
243-
}
244-
continue
245-
}
246-
}
247-
248-
if cluster.Spec.Topology.Workers == nil {
249-
continue
250-
}
251-
252-
for _, md := range cluster.Spec.Topology.Workers.MachineDeployments {
253-
// Continue if there are no variable overrides.
254-
if md.Variables == nil || len(md.Variables.Overrides) == 0 {
255-
continue
256-
}
257-
258-
for _, c := range md.Variables.Overrides {
259-
// copy variable to avoid memory aliasing.
260-
clusterVar := c
261-
262-
// 1) Error if a variable with a schema altered in the update is no longer valid on the Cluster.
263-
if alteredVar, ok := varsDiff[variableValidationKey{clusterVar.Name, altered}]; ok {
264-
if errs := variables.ValidateClusterVariable(&clusterVar, alteredVar, field.NewPath("")); len(errs) > 0 {
265-
errorInfo.add(fmt.Sprintf("%s/%s", md.Name, alteredVar.Name), altered, cluster.Name)
266-
}
267-
continue
268-
}
269-
270-
// 2) Error if a variable removed in the update is still in use in some Clusters.
271-
if _, ok := varsDiff[variableValidationKey{clusterVar.Name, removed}]; ok {
272-
errorInfo.add(fmt.Sprintf("%s/%s", md.Name, clusterVar.Name), removed, cluster.Name)
273-
continue
274-
}
275-
276-
// 3) Error if a variable has been added in the update check is no longer valid on the Cluster.
277-
// NOTE: This can't occur in normal circumstances as a variable must be defined in a ClusterClass in order to be introduced in
278-
// a Cluster. This check may catch errors in cases involving broken Clusters.
279-
if addedVar, ok := varsDiff[variableValidationKey{clusterVar.Name, added}]; ok {
280-
if errs := variables.ValidateClusterVariable(&clusterVar, addedVar, field.NewPath("")); len(errs) > 0 {
281-
errorInfo.add(fmt.Sprintf("%s/%s", md.Name, addedVar.Name), added, cluster.Name)
282-
}
283-
continue
284-
}
285-
}
286-
}
287-
}
288-
289-
// 4) Error if a required variable is not defined top-level in every cluster using the ClusterClass.
290-
for v := range varsDiff {
291-
if v.action == required {
292-
clustersMissingVariable := clustersWithoutVar(allClusters, tracker[v.name])
293-
if len(clustersMissingVariable) > 0 {
294-
errorInfo.add(v.name, v.action, clustersMissingVariable...)
295-
}
296-
}
297-
}
298-
299-
// Aggregate the errors from the validation checks and return one error message of each type for each variable with a list of violating clusters.
300-
return aggregateErrors(errorInfo, clusterClassVariablesIndex, path)
301-
}
302-
303186
// validateUpdatesToMachineHealthCheckClasses checks if the updates made to MachineHealthChecks are valid.
304187
// It makes sure that if a MachineHealthCheck definition is dropped from the ClusterClass then none of the
305188
// clusters using the ClusterClass rely on it to create a MachineHealthCheck.
@@ -366,106 +249,6 @@ func validateUpdatesToMachineHealthCheckClasses(clusters []clusterv1.Cluster, ol
366249
return allErrs
367250
}
368251

369-
type errorAggregator map[variableValidationKey][]string
370-
371-
func (e errorAggregator) add(name string, action variableValidationType, references ...string) {
372-
e[variableValidationKey{name, action}] = append(e[variableValidationKey{name, action}], references...)
373-
}
374-
375-
func aggregateErrors(errs map[variableValidationKey][]string, clusterClassVariablesIndex map[string]int, path *field.Path) field.ErrorList {
376-
var allErrs, addedErrs, alteredErrs, removedErrs, requiredErrs field.ErrorList
377-
// Look through the errors and append aggregated error messages naming the Clusters blocking changes to existing variables.
378-
for variable, clusters := range errs {
379-
switch variable.action {
380-
case altered:
381-
alteredErrs = append(alteredErrs,
382-
field.Forbidden(path.Index(clusterClassVariablesIndex[variable.name]),
383-
fmt.Sprintf("variable schema change for %s is invalid. It failed validation in Clusters %v", variable.name, clusters),
384-
))
385-
case removed:
386-
removedErrs = append(removedErrs,
387-
field.Forbidden(path.Index(clusterClassVariablesIndex[variable.name]),
388-
fmt.Sprintf("variable %s can not be removed. It is used in Clusters %v", variable.name, clusters),
389-
))
390-
case added:
391-
addedErrs = append(addedErrs,
392-
field.Forbidden(path.Index(clusterClassVariablesIndex[variable.name]),
393-
fmt.Sprintf("variable %s can not be added. It failed validation in Clusters %v", variable.name, clusters),
394-
))
395-
case required:
396-
requiredErrs = append(requiredErrs,
397-
field.Forbidden(path.Index(clusterClassVariablesIndex[variable.name]),
398-
fmt.Sprintf("variable %v is required but is not defined in Clusters %v", variable.name, clusters),
399-
))
400-
}
401-
}
402-
403-
// Add the slices of errors one by one to maintain a defined order.
404-
allErrs = append(allErrs, alteredErrs...)
405-
allErrs = append(allErrs, removedErrs...)
406-
allErrs = append(allErrs, addedErrs...)
407-
allErrs = append(allErrs, requiredErrs...)
408-
409-
return allErrs
410-
}
411-
412-
func clustersWithoutVar(allClusters, clustersWithVar []string) []string {
413-
clustersWithVarMap := make(map[string]interface{})
414-
for _, cluster := range clustersWithVar {
415-
clustersWithVarMap[cluster] = nil
416-
}
417-
var clusterDiff []string
418-
for _, cluster := range allClusters {
419-
if _, found := clustersWithVarMap[cluster]; !found {
420-
clusterDiff = append(clusterDiff, cluster)
421-
}
422-
}
423-
return clusterDiff
424-
}
425-
426-
type variableValidationType int
427-
428-
const (
429-
added variableValidationType = iota
430-
altered
431-
removed
432-
required
433-
)
434-
435-
// variableValidationKey holds the name of the variable and a validation action to perform on it.
436-
type variableValidationKey struct {
437-
name string
438-
action variableValidationType
439-
}
440-
441-
// getClusterClassVariablesForValidation returns a struct with the four classes of ClusterClass Variables that require validation:
442-
// - added which have been added to the ClusterClass on update.
443-
// - altered which have had their schemas changed on update.
444-
// - removed which have been removed from the ClusterClass on update.
445-
// - required (with 'required' : true) from the new ClusterClass.
446-
func getClusterClassVariablesForValidation(oldVars, newVars map[string]*clusterv1.ClusterClassVariable) map[variableValidationKey]*clusterv1.ClusterClassVariable {
447-
out := map[variableValidationKey]*clusterv1.ClusterClassVariable{}
448-
449-
// Compare the old Variable map to the new one to discover which variables have been removed and which have been altered.
450-
for k, v := range oldVars {
451-
if _, ok := newVars[k]; !ok {
452-
out[variableValidationKey{action: removed, name: k}] = v
453-
} else if !reflect.DeepEqual(v.Schema, newVars[k].Schema) {
454-
out[variableValidationKey{action: altered, name: k}] = newVars[k]
455-
}
456-
}
457-
// Compare the new ClusterClassVariables to the new ClusterClassVariables to find out what variables have been added and which are now required.
458-
for k, v := range newVars {
459-
if _, ok := oldVars[k]; !ok {
460-
out[variableValidationKey{action: added, name: k}] = v
461-
}
462-
if v.Required {
463-
out[variableValidationKey{action: required, name: v.Name}] = v
464-
}
465-
}
466-
return out
467-
}
468-
469252
func (webhook *ClusterClass) validateRemovedMachineDeploymentClassesAreNotUsed(clusters []clusterv1.Cluster, oldClusterClass, newClusterClass *clusterv1.ClusterClass) field.ErrorList {
470253
var allErrs field.ErrorList
471254

0 commit comments

Comments
 (0)