Skip to content

Commit 042a2a6

Browse files
Add handling for variables with conflicting definitions
1 parent f144cc6 commit 042a2a6

File tree

8 files changed

+1460
-591
lines changed

8 files changed

+1460
-591
lines changed

internal/controllers/clusterclass/clusterclass_controller.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package clusterclass
1919
import (
2020
"context"
2121
"fmt"
22+
"reflect"
2223
"strings"
2324

2425
"github.com/pkg/errors"
@@ -213,12 +214,10 @@ func (r *Reconciler) reconcileExternalReferences(ctx context.Context, clusterCla
213214

214215
func (r *Reconciler) reconcileVariables(ctx context.Context, clusterClass *clusterv1.ClusterClass) error {
215216
errs := []error{}
216-
uniqueVars := map[string]bool{}
217-
ccStatusVars := []clusterv1.ClusterClassStatusVariable{}
217+
ccStatusVars := map[string]clusterv1.ClusterClassStatusVariable{}
218218
// Add inline variable definitions to the ClusterClass status.
219219
for _, variable := range clusterClass.Spec.Variables {
220-
uniqueVars[variable.Name] = true
221-
ccStatusVars = append(ccStatusVars, statusVariableFromClusterClassVariable(variable, clusterv1.VariableDefinitionFromInline))
220+
ccStatusVars[variable.Name] = addNewStatusVariable(variable, clusterv1.VariableDefinitionFromInline)
222221
}
223222

224223
// If RuntimeSDK is enabled call the DiscoverVariables hook for all associated Runtime Extensions and add the variables
@@ -243,13 +242,12 @@ func (r *Reconciler) reconcileVariables(ctx context.Context, clusterClass *clust
243242
}
244243
if resp.Variables != nil {
245244
for _, variable := range resp.Variables {
246-
// TODO: Variables should be validated and deduplicated based on their provided definition.
247-
if _, ok := uniqueVars[variable.Name]; ok {
248-
errs = append(errs, errors.Errorf("duplicate variable name %s discovered in patch: %s", variable.Name, patch.Name))
245+
// If a variable of the same name already exists add it to the existing definition list.
246+
if _, ok := ccStatusVars[variable.Name]; ok {
247+
ccStatusVars[variable.Name] = addDefinitiontoExistingStatusVariable(variable, patch.Name, ccStatusVars[variable.Name])
249248
continue
250249
}
251-
uniqueVars[variable.Name] = true
252-
ccStatusVars = append(ccStatusVars, statusVariableFromClusterClassVariable(variable, patch.Name))
250+
ccStatusVars[variable.Name] = addNewStatusVariable(variable, patch.Name)
253251
}
254252
}
255253
}
@@ -260,10 +258,15 @@ func (r *Reconciler) reconcileVariables(ctx context.Context, clusterClass *clust
260258
"VariableDiscovery failed: %s", kerrors.NewAggregate(errs))
261259
return errors.Wrapf(kerrors.NewAggregate(errs), "failed to discover variables for ClusterClass %s", clusterClass.Name)
262260
}
263-
clusterClass.Status.Variables = ccStatusVars
261+
statusVarList := []clusterv1.ClusterClassStatusVariable{}
262+
for _, variable := range ccStatusVars {
263+
statusVarList = append(statusVarList, variable)
264+
}
265+
clusterClass.Status.Variables = statusVarList
264266
conditions.MarkTrue(clusterClass, clusterv1.ClusterClassVariablesReconciledCondition)
265267
return nil
266268
}
269+
267270
func reconcileConditions(clusterClass *clusterv1.ClusterClass, outdatedRefs map[*corev1.ObjectReference]*corev1.ObjectReference) {
268271
if len(outdatedRefs) > 0 {
269272
var msg []string
@@ -288,10 +291,9 @@ func reconcileConditions(clusterClass *clusterv1.ClusterClass, outdatedRefs map[
288291
)
289292
}
290293

291-
func statusVariableFromClusterClassVariable(variable clusterv1.ClusterClassVariable, from string) clusterv1.ClusterClassStatusVariable {
294+
func addNewStatusVariable(variable clusterv1.ClusterClassVariable, from string) clusterv1.ClusterClassStatusVariable {
292295
return clusterv1.ClusterClassStatusVariable{
293-
Name: variable.Name,
294-
// TODO: In a future iteration this should be true where definitions are equal.
296+
Name: variable.Name,
295297
DefinitionsConflict: false,
296298
Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
297299
{
@@ -302,6 +304,22 @@ func statusVariableFromClusterClassVariable(variable clusterv1.ClusterClassVaria
302304
}}
303305
}
304306

307+
func addDefinitiontoExistingStatusVariable(variable clusterv1.ClusterClassVariable, from string, existingVariable clusterv1.ClusterClassStatusVariable) clusterv1.ClusterClassStatusVariable {
308+
combinedVariable := existingVariable.DeepCopy()
309+
newVariableDefinition := clusterv1.ClusterClassStatusVariableDefinition{
310+
From: from,
311+
Required: variable.Required,
312+
Schema: variable.Schema,
313+
}
314+
combinedVariable.Definitions = append(existingVariable.Definitions, newVariableDefinition)
315+
316+
// If the new definition is different from any existing definition, set DefinitionsConflict to true.
317+
if !reflect.DeepEqual(combinedVariable.Definitions[0], newVariableDefinition) {
318+
combinedVariable.DefinitionsConflict = true
319+
}
320+
return *combinedVariable
321+
}
322+
305323
func refString(ref *corev1.ObjectReference) string {
306324
return fmt.Sprintf("%s %s/%s", ref.GroupVersionKind().String(), ref.Namespace, ref.Name)
307325
}

internal/controllers/topology/cluster/cluster_controller_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"testing"
2323
"time"
2424

25+
"github.com/google/go-cmp/cmp"
2526
. "github.com/onsi/gomega"
2627
corev1 "k8s.io/api/core/v1"
2728
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -1114,7 +1115,7 @@ func TestReconciler_DefaultCluster(t *testing.T) {
11141115
Build(),
11151116
wantCluster: clusterBuilder.DeepCopy().
11161117
WithTopology(topologyBase.DeepCopy().WithVariables(
1117-
clusterv1.ClusterVariable{Name: "location", Value: apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}}).
1118+
clusterv1.ClusterVariable{Name: "location", Value: apiextensionsv1.JSON{Raw: []byte(`"us-east"`)}, DefinitionFrom: clusterv1.VariableDefinitionFromInline}).
11181119
Build()).
11191120
Build(),
11201121
},
@@ -1234,7 +1235,7 @@ func TestReconciler_DefaultCluster(t *testing.T) {
12341235
got := &clusterv1.Cluster{}
12351236
g.Expect(fakeClient.Get(ctx, client.ObjectKey{Name: tt.initialCluster.Name, Namespace: tt.initialCluster.Namespace}, got)).To(Succeed())
12361237
// Compare the spec of the two clusters to ensure that variables are defaulted correctly.
1237-
g.Expect(reflect.DeepEqual(got.Spec, tt.wantCluster.Spec)).To(BeTrue())
1238+
g.Expect(reflect.DeepEqual(got.Spec, tt.wantCluster.Spec)).To(BeTrue(), cmp.Diff(got.Spec, tt.wantCluster.Spec))
12381239
})
12391240
}
12401241
}

internal/topology/variables/cluster_variable_defaulting.go

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,66 +30,89 @@ import (
3030
)
3131

3232
// DefaultClusterVariables defaults ClusterVariables.
33-
func DefaultClusterVariables(clusterVariables []clusterv1.ClusterVariable, clusterClassVariables []clusterv1.ClusterClassVariable, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) {
33+
func DefaultClusterVariables(clusterVariables []clusterv1.ClusterVariable, clusterClassVariables []clusterv1.ClusterClassStatusVariable, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) {
3434
return defaultClusterVariables(clusterVariables, clusterClassVariables, true, fldPath)
3535
}
3636

3737
// DefaultMachineDeploymentVariables defaults MachineDeploymentVariables.
38-
func DefaultMachineDeploymentVariables(machineDeploymentVariables []clusterv1.ClusterVariable, clusterClassVariables []clusterv1.ClusterClassVariable, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) {
38+
func DefaultMachineDeploymentVariables(machineDeploymentVariables []clusterv1.ClusterVariable, clusterClassVariables []clusterv1.ClusterClassStatusVariable, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) {
3939
return defaultClusterVariables(machineDeploymentVariables, clusterClassVariables, false, fldPath)
4040
}
4141

4242
// defaultClusterVariables defaults variables.
4343
// If they do not exist yet, they are created if createVariables is set.
44-
func defaultClusterVariables(clusterVariables []clusterv1.ClusterVariable, clusterClassVariables []clusterv1.ClusterClassVariable, createVariables bool, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) {
44+
func defaultClusterVariables(clusterVariables []clusterv1.ClusterVariable, clusterClassVariables []clusterv1.ClusterClassStatusVariable, createVariables bool, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) {
4545
var allErrs field.ErrorList
4646

47-
// Build maps for easier and faster access.
48-
clusterVariablesMap := getClusterVariablesMap(clusterVariables)
49-
clusterClassVariablesMap := getClusterClassVariablesMap(clusterClassVariables)
50-
51-
// Validate that all variables in the Cluster are defined in the ClusterClass.
52-
// Note: If we don't validate this, we would get a nil pointer dereference below.
53-
allErrs = append(allErrs, validateClusterVariablesDefined(clusterVariables, clusterClassVariablesMap, fldPath)...)
54-
if len(allErrs) > 0 {
55-
return nil, allErrs
47+
// Get a map of ClusterVariables and ensure that variables are not defined more than once in Cluster spec.
48+
clusterVariablesMap, err := getClusterVariablesMap(clusterVariables)
49+
if err != nil {
50+
return nil, append(allErrs, field.Invalid(fldPath, clusterVariables,
51+
fmt.Sprintf("cluster variables not valid: %s", err)))
5652
}
5753

54+
// Get a map of ClusterClassVariables for each variable name and definition.
55+
clusterClassVariablesMap := getClusterClassVariablesMap(clusterClassVariables)
56+
5857
// allVariables is used to get a full correctly ordered list of variables.
59-
allVariables := []string{}
58+
allVariables := []clusterv1.ClusterVariable{}
6059
// Add any ClusterVariables that already exist.
61-
for _, variable := range clusterVariables {
62-
allVariables = append(allVariables, variable.Name)
63-
}
60+
allVariables = append(allVariables, clusterVariables...)
61+
6462
// Add variables from the ClusterClass, which currently don't exist on the Cluster.
6563
for _, variable := range clusterClassVariables {
66-
// Continue if the ClusterClass variable already exists.
67-
if _, ok := clusterVariablesMap[variable.Name]; ok {
68-
continue
64+
for _, definition := range variable.Definitions {
65+
if _, ok := clusterVariablesMap[variable.Name]; ok {
66+
// Continue if the Cluster variable with a definitionFrom this ClusterClass variable exists.
67+
if _, ok := clusterVariablesMap[variable.Name][definition.From]; ok {
68+
continue
69+
}
70+
// Continue if the Cluster variable with an empty definitionFrom exists. The user intention here is to
71+
// use the default value from a ClusterClass variable with no conflicting variables.
72+
if _, ok := clusterVariablesMap[variable.Name][emptyVariableDefinitionFrom]; ok {
73+
continue
74+
}
75+
}
76+
allVariables = append(allVariables, clusterv1.ClusterVariable{
77+
Name: variable.Name,
78+
DefinitionFrom: definition.From,
79+
})
6980
}
70-
71-
allVariables = append(allVariables, variable.Name)
7281
}
7382

7483
// Default all variables.
7584
defaultedClusterVariables := []clusterv1.ClusterVariable{}
76-
for i, variableName := range allVariables {
77-
clusterClassVariable := clusterClassVariablesMap[variableName]
78-
clusterVariable := clusterVariablesMap[variableName]
85+
for i, variable := range allVariables {
86+
clusterClassVariable, found := clusterClassVariablesMap[ccVariableMapKey{name: variable.Name, from: variable.DefinitionFrom}]
87+
if !found {
88+
allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("name"), variable.Name,
89+
fmt.Sprintf("variable with name %q from %q is not defined in ClusterClass `status.Variables`.", variable.Name, variable.DefinitionFrom)))
90+
continue
91+
}
7992

80-
defaultedClusterVariable, errs := defaultClusterVariable(clusterVariable, clusterClassVariable, fldPath.Index(i), createVariables)
93+
// If the variable is defined in the Cluster spec get the definition.
94+
var clusterVariable *clusterv1.ClusterVariable
95+
if variablesWithName, ok := clusterVariablesMap[variable.Name]; ok {
96+
if variableFromDefinition, ok := variablesWithName[variable.DefinitionFrom]; ok {
97+
clusterVariable = &variableFromDefinition
98+
}
99+
}
100+
101+
defaultedClusterVariable, errs := defaultClusterVariable(clusterVariable, &clusterv1.ClusterClassVariable{
102+
Name: variable.Name,
103+
Required: clusterClassVariable.Required,
104+
Schema: clusterClassVariable.Schema,
105+
}, clusterClassVariable.From, fldPath.Index(i), createVariables)
81106
if len(errs) > 0 {
82107
allErrs = append(allErrs, errs...)
83108
continue
84109
}
85-
86110
// Continue if there is no defaulted variable.
87111
// NOTE: This happens when the variable doesn't exist on the CLuster before and
88112
// there is no top-level default value.
89113
if defaultedClusterVariable == nil {
90114
continue
91115
}
92-
93116
defaultedClusterVariables = append(defaultedClusterVariables, *defaultedClusterVariable)
94117
}
95118

@@ -101,7 +124,7 @@ func defaultClusterVariables(clusterVariables []clusterv1.ClusterVariable, clust
101124
}
102125

103126
// defaultClusterVariable defaults a clusterVariable based on the default value in the clusterClassVariable.
104-
func defaultClusterVariable(clusterVariable *clusterv1.ClusterVariable, clusterClassVariable *clusterv1.ClusterClassVariable, fldPath *field.Path, createVariable bool) (*clusterv1.ClusterVariable, field.ErrorList) {
127+
func defaultClusterVariable(clusterVariable *clusterv1.ClusterVariable, clusterClassVariable *clusterv1.ClusterClassVariable, definitionFrom string, fldPath *field.Path, createVariable bool) (*clusterv1.ClusterVariable, field.ErrorList) {
105128
if clusterVariable == nil {
106129
// Return if the variable does not exist yet and createVariable is false.
107130
if !createVariable {
@@ -159,10 +182,12 @@ func defaultClusterVariable(clusterVariable *clusterv1.ClusterVariable, clusterC
159182
return nil, field.ErrorList{field.Invalid(fldPath, "",
160183
fmt.Sprintf("failed to marshal default value of variable %q: %v", clusterClassVariable.Name, err))}
161184
}
162-
return &clusterv1.ClusterVariable{
185+
v := &clusterv1.ClusterVariable{
163186
Name: clusterClassVariable.Name,
164187
Value: apiextensionsv1.JSON{
165188
Raw: defaultedVariableValue,
166189
},
167-
}, nil
190+
DefinitionFrom: definitionFrom,
191+
}
192+
return v, nil
168193
}

0 commit comments

Comments
 (0)