Skip to content

Commit 82ab810

Browse files
authored
Merge pull request #5928 from sbueringer/pr-md-variables
✨ ClusterClass: implement MachineDeployment variable overrides
2 parents 146988a + a763938 commit 82ab810

16 files changed

+1022
-76
lines changed

api/v1alpha4/conversion.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error {
4242
if restored.Spec.Topology.Workers != nil {
4343
for i := range restored.Spec.Topology.Workers.MachineDeployments {
4444
dst.Spec.Topology.Workers.MachineDeployments[i].FailureDomain = restored.Spec.Topology.Workers.MachineDeployments[i].FailureDomain
45+
dst.Spec.Topology.Workers.MachineDeployments[i].Variables = restored.Spec.Topology.Workers.MachineDeployments[i].Variables
4546
}
4647
}
4748
}

api/v1alpha4/zz_generated.conversion.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta1/cluster_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ type MachineDeploymentTopology struct {
154154
// of this value.
155155
// +optional
156156
Replicas *int32 `json:"replicas,omitempty"`
157+
158+
// Variables can be used to customize the MachineDeployment through patches.
159+
// +optional
160+
Variables *MachineDeploymentVariables `json:"variables,omitempty"`
157161
}
158162

159163
// ClusterVariable can be used to customize the Cluster through
@@ -173,6 +177,13 @@ type ClusterVariable struct {
173177
Value apiextensionsv1.JSON `json:"value"`
174178
}
175179

180+
// MachineDeploymentVariables can be used to provide variables for a specific MachineDeployment.
181+
type MachineDeploymentVariables struct {
182+
// Overrides can be used to override Cluster level variables.
183+
// +optional
184+
Overrides []ClusterVariable `json:"overrides,omitempty"`
185+
}
186+
176187
// ANCHOR_END: ClusterSpec
177188

178189
// ANCHOR: ClusterNetwork

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/cluster.x-k8s.io_clusters.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,40 @@ spec:
968968
of this value.
969969
format: int32
970970
type: integer
971+
variables:
972+
description: Variables can be used to customize the
973+
MachineDeployment through patches.
974+
properties:
975+
overrides:
976+
description: Overrides can be used to override Cluster
977+
level variables.
978+
items:
979+
description: ClusterVariable can be used to customize
980+
the Cluster through patches. It must comply
981+
to the corresponding ClusterClassVariable defined
982+
in the ClusterClass.
983+
properties:
984+
name:
985+
description: Name of the variable.
986+
type: string
987+
value:
988+
description: 'Value of the variable. Note:
989+
the value will be validated against the
990+
schema of the corresponding ClusterClassVariable
991+
from the ClusterClass. Note: We have to
992+
use apiextensionsv1.JSON instead of a custom
993+
JSON type, because controller-tools has
994+
a hard-coded schema for apiextensionsv1.JSON
995+
which cannot be produced by another type
996+
via controller-tools, i.e. it is not possible
997+
to have no type field. Ref: https://github.com/kubernetes-sigs/controller-tools/blob/d0e03a142d0ecdd5491593e941ee1d6b5d91dba6/pkg/crd/known_types.go#L106-L111'
998+
x-kubernetes-preserve-unknown-fields: true
999+
required:
1000+
- name
1001+
- value
1002+
type: object
1003+
type: array
1004+
type: object
9711005
required:
9721006
- class
9731007
- name

internal/controllers/topology/cluster/patches/variables/variables.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ func ControlPlane(controlPlaneTopology *clusterv1.ControlPlaneTopology, controlP
176176
func MachineDeployment(mdTopology *clusterv1.MachineDeploymentTopology, md *clusterv1.MachineDeployment) (VariableMap, error) {
177177
variables := VariableMap{}
178178

179+
// Add variables overrides for the MachineDeployment.
180+
if mdTopology.Variables != nil {
181+
for _, variable := range mdTopology.Variables.Overrides {
182+
variables[variable.Name] = variable.Value
183+
}
184+
}
185+
179186
// Construct builtin variable.
180187
builtin := Builtins{
181188
MachineDeployment: &MachineDeploymentBuiltins{

internal/controllers/topology/cluster/patches/variables/variables_test.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ func TestGlobal(t *testing.T) {
4949
Value: toJSON("8"),
5050
},
5151
{
52-
// This is blocked by a webhook, but let's make sure we overwrite
53-
// the user-defined variable with the builtin variable anyway.
52+
// This is blocked by a webhook, but let's make sure that the user-defined
53+
// variable is overwritten by the builtin variable anyway.
5454
Name: "builtin",
5555
Value: toJSON("8"),
5656
},
@@ -137,6 +137,35 @@ func TestMachineDeployment(t *testing.T) {
137137
}{
138138
{
139139
name: "Should calculate MachineDeployment variables",
140+
mdTopology: &clusterv1.MachineDeploymentTopology{
141+
Replicas: pointer.Int32(3),
142+
Name: "md-topology",
143+
Class: "md-class",
144+
Variables: &clusterv1.MachineDeploymentVariables{
145+
Overrides: []clusterv1.ClusterVariable{
146+
{
147+
Name: "location",
148+
Value: toJSON("\"us-central\""),
149+
},
150+
{
151+
Name: "cpu",
152+
Value: toJSON("8"),
153+
},
154+
},
155+
},
156+
},
157+
md: builder.MachineDeployment(metav1.NamespaceDefault, "md1").
158+
WithReplicas(3).
159+
WithVersion("v1.21.1").
160+
Build(),
161+
want: VariableMap{
162+
"location": toJSON("\"us-central\""),
163+
"cpu": toJSON("8"),
164+
BuiltinsName: toJSON("{\"machineDeployment\":{\"version\":\"v1.21.1\",\"class\":\"md-class\",\"name\":\"md1\",\"topologyName\":\"md-topology\",\"replicas\":3}}"),
165+
},
166+
},
167+
{
168+
name: "Should calculate MachineDeployment variables (without overrides)",
140169
mdTopology: &clusterv1.MachineDeploymentTopology{
141170
Replicas: pointer.Int32(3),
142171
Name: "md-topology",
@@ -155,11 +184,25 @@ func TestMachineDeployment(t *testing.T) {
155184
mdTopology: &clusterv1.MachineDeploymentTopology{
156185
Name: "md-topology",
157186
Class: "md-class",
187+
Variables: &clusterv1.MachineDeploymentVariables{
188+
Overrides: []clusterv1.ClusterVariable{
189+
{
190+
Name: "location",
191+
Value: toJSON("\"us-central\""),
192+
},
193+
{
194+
Name: "cpu",
195+
Value: toJSON("8"),
196+
},
197+
},
198+
},
158199
},
159200
md: builder.MachineDeployment(metav1.NamespaceDefault, "md1").
160201
WithVersion("v1.21.1").
161202
Build(),
162203
want: VariableMap{
204+
"location": toJSON("\"us-central\""),
205+
"cpu": toJSON("8"),
163206
BuiltinsName: toJSON("{\"machineDeployment\":{\"version\":\"v1.21.1\",\"class\":\"md-class\",\"name\":\"md1\",\"topologyName\":\"md-topology\"}}"),
164207
},
165208
},

internal/test/builder/builders.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,10 @@ func (c *ClusterTopologyBuilder) Build() *clusterv1.Topology {
157157

158158
// MachineDeploymentTopologyBuilder holds the values needed to create a testable MachineDeploymentTopology.
159159
type MachineDeploymentTopologyBuilder struct {
160-
class string
161-
name string
162-
replicas *int32
160+
class string
161+
name string
162+
replicas *int32
163+
variables []clusterv1.ClusterVariable
163164
}
164165

165166
// MachineDeploymentTopology returns a builder used to create a testable MachineDeploymentTopology.
@@ -181,13 +182,27 @@ func (m *MachineDeploymentTopologyBuilder) WithReplicas(replicas int32) *Machine
181182
return m
182183
}
183184

185+
// WithVariables adds variables used as the MachineDeploymentTopology variables value.
186+
func (m *MachineDeploymentTopologyBuilder) WithVariables(variables ...clusterv1.ClusterVariable) *MachineDeploymentTopologyBuilder {
187+
m.variables = variables
188+
return m
189+
}
190+
184191
// Build returns a testable MachineDeploymentTopology with any values passed to the builder.
185192
func (m *MachineDeploymentTopologyBuilder) Build() clusterv1.MachineDeploymentTopology {
186-
return clusterv1.MachineDeploymentTopology{
193+
md := clusterv1.MachineDeploymentTopology{
187194
Class: m.class,
188195
Name: m.name,
189196
Replicas: m.replicas,
190197
}
198+
199+
if len(m.variables) > 0 {
200+
md.Variables = &clusterv1.MachineDeploymentVariables{
201+
Overrides: m.variables,
202+
}
203+
}
204+
205+
return md
191206
}
192207

193208
// ClusterClassBuilder holds the variables and objects required to build a clusterv1.ClusterClass.

internal/topology/variables/cluster_variable_defaulting.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,32 @@ import (
2929
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3030
)
3131

32-
// DefaultClusterVariables defaults variables which do not exist in clusterVariable, if they
33-
// have a default value in the corresponding schema in clusterClassVariable.
32+
// DefaultClusterVariables defaults ClusterVariables.
3433
func DefaultClusterVariables(clusterVariables []clusterv1.ClusterVariable, clusterClassVariables []clusterv1.ClusterClassVariable, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) {
34+
return defaultClusterVariables(clusterVariables, clusterClassVariables, true, fldPath)
35+
}
36+
37+
// DefaultMachineDeploymentVariables defaults MachineDeploymentVariables.
38+
func DefaultMachineDeploymentVariables(clusterVariables []clusterv1.ClusterVariable, clusterClassVariables []clusterv1.ClusterClassVariable, fldPath *field.Path) ([]clusterv1.ClusterVariable, field.ErrorList) {
39+
return defaultClusterVariables(clusterVariables, clusterClassVariables, false, fldPath)
40+
}
41+
42+
// defaultClusterVariables defaults variables.
43+
// 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) {
3545
var allErrs field.ErrorList
3646

3747
// Build maps for easier and faster access.
3848
clusterVariablesMap := getClusterVariablesMap(clusterVariables)
3949
clusterClassVariablesMap := getClusterClassVariablesMap(clusterClassVariables)
4050

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
56+
}
57+
4158
// allVariables is used to get a full correctly ordered list of variables.
4259
allVariables := []string{}
4360
// Add any ClusterVariables that already exist.
@@ -60,7 +77,7 @@ func DefaultClusterVariables(clusterVariables []clusterv1.ClusterVariable, clust
6077
clusterClassVariable := clusterClassVariablesMap[variableName]
6178
clusterVariable := clusterVariablesMap[variableName]
6279

63-
defaultedClusterVariable, errs := defaultClusterVariable(clusterVariable, clusterClassVariable, fldPath.Index(i))
80+
defaultedClusterVariable, errs := defaultClusterVariable(clusterVariable, clusterClassVariable, fldPath.Index(i), createVariables)
6481
if len(errs) > 0 {
6582
allErrs = append(allErrs, errs...)
6683
continue
@@ -84,10 +101,16 @@ func DefaultClusterVariables(clusterVariables []clusterv1.ClusterVariable, clust
84101
}
85102

86103
// defaultClusterVariable defaults a clusterVariable based on the default value in the clusterClassVariable.
87-
func defaultClusterVariable(clusterVariable *clusterv1.ClusterVariable, clusterClassVariable *clusterv1.ClusterClassVariable, fldPath *field.Path) (*clusterv1.ClusterVariable, field.ErrorList) {
88-
// Return if the variable does not exist yet and there is no top-level default value.
89-
if clusterVariable == nil && clusterClassVariable.Schema.OpenAPIV3Schema.Default == nil {
90-
return nil, nil
104+
func defaultClusterVariable(clusterVariable *clusterv1.ClusterVariable, clusterClassVariable *clusterv1.ClusterClassVariable, fldPath *field.Path, createVariable bool) (*clusterv1.ClusterVariable, field.ErrorList) {
105+
if clusterVariable == nil {
106+
// Return if the variable does not exist yet and createVariable is false.
107+
if !createVariable {
108+
return nil, nil
109+
}
110+
// Return if the variable does not exist yet and there is no top-level default value.
111+
if clusterClassVariable.Schema.OpenAPIV3Schema.Default == nil {
112+
return nil, nil
113+
}
91114
}
92115

93116
// Convert schema to Kubernetes APIExtensions schema.

0 commit comments

Comments
 (0)