Skip to content

Commit 41b0fb6

Browse files
authored
Merge pull request #8128 from killianmuldoon/pr-exvars-per-definition-patches
⚠️ Add filter to associate variables with specific patches
2 parents 9589de4 + 090c27b commit 41b0fb6

File tree

4 files changed

+436
-52
lines changed

4 files changed

+436
-52
lines changed

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

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
jsonpatch "github.com/evanphx/json-patch/v5"
2727
"github.com/pkg/errors"
2828
kerrors "k8s.io/apimachinery/pkg/util/errors"
29+
"k8s.io/klog/v2"
2930

3031
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3132
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
@@ -83,6 +84,14 @@ func (e *engine) Apply(ctx context.Context, blueprint *scope.ClusterBlueprint, d
8384
clusterClassPatch := blueprint.ClusterClass.Spec.Patches[i]
8485
ctx, log := log.WithValues("patch", clusterClassPatch.Name).Into(ctx)
8586

87+
definitionFrom := clusterClassPatch.Name
88+
// If this isn't an external patch, use the inline patch name.
89+
if clusterClassPatch.External == nil {
90+
definitionFrom = clusterv1.VariableDefinitionFromInline
91+
}
92+
if err := addVariablesForPatch(blueprint, desired, req, definitionFrom); err != nil {
93+
return errors.Wrapf(err, "failed to calculate variables for patch %q", clusterClassPatch.Name)
94+
}
8695
log.V(5).Infof("Applying patch to templates")
8796

8897
// Create patch generator for the current patch.
@@ -139,23 +148,86 @@ func (e *engine) Apply(ctx context.Context, blueprint *scope.ClusterBlueprint, d
139148
return nil
140149
}
141150

151+
// addVariablesForPatch adds variables for a given ClusterClassPatch to the items in the PatchRequest.
152+
func addVariablesForPatch(blueprint *scope.ClusterBlueprint, desired *scope.ClusterState, req *runtimehooksv1.GeneratePatchesRequest, definitionFrom string) error {
153+
// If there is no definitionFrom return an error.
154+
if definitionFrom == "" {
155+
return errors.New("failed to calculate variables: no patch name provided")
156+
}
157+
158+
patchVariableDefinitions := definitionsForPatch(blueprint, definitionFrom)
159+
// Calculate global variables.
160+
globalVariables, err := variables.Global(blueprint.Topology, desired.Cluster, definitionFrom, patchVariableDefinitions)
161+
if err != nil {
162+
return errors.Wrapf(err, "failed to calculate global variables")
163+
}
164+
req.Variables = globalVariables
165+
166+
// Calculate the Control Plane variables.
167+
controlPlaneVariables, err := variables.ControlPlane(&blueprint.Topology.ControlPlane, desired.ControlPlane.Object, desired.ControlPlane.InfrastructureMachineTemplate)
168+
if err != nil {
169+
return errors.Wrapf(err, "failed to calculate ControlPlane variables")
170+
}
171+
172+
mdStateIndex := map[string]*scope.MachineDeploymentState{}
173+
for _, md := range desired.MachineDeployments {
174+
mdStateIndex[md.Object.Name] = md
175+
}
176+
for i, item := range req.Items {
177+
// If the item is a Control Plane add the Control Plane variables.
178+
if item.HolderReference.FieldPath == "spec.controlPlaneRef" {
179+
item.Variables = controlPlaneVariables
180+
}
181+
// If the item holder reference is a Control Plane machine add the Control Plane variables.
182+
if blueprint.HasControlPlaneInfrastructureMachine() &&
183+
item.HolderReference.FieldPath == strings.Join(contract.ControlPlane().MachineTemplate().InfrastructureRef().Path(), ".") {
184+
item.Variables = controlPlaneVariables
185+
}
186+
// If the item holder reference is a MachineDeployment calculate the variables for each MachineDeploymentTopology
187+
// and add them to the variables for the MachineDeployment.
188+
if item.HolderReference.Kind == "MachineDeployment" {
189+
md, ok := mdStateIndex[item.HolderReference.Name]
190+
if !ok {
191+
return errors.Errorf("could not find desired state for MachineDeployment %s", klog.KObj(md.Object))
192+
}
193+
mdTopology, err := getMDTopologyFromMD(blueprint, md.Object)
194+
if err != nil {
195+
return err
196+
}
197+
198+
// Calculate MachineDeployment variables.
199+
mdVariables, err := variables.MachineDeployment(mdTopology, md.Object, md.BootstrapTemplate, md.InfrastructureMachineTemplate, definitionFrom, patchVariableDefinitions)
200+
if err != nil {
201+
return errors.Wrapf(err, "failed to calculate variables for %s", klog.KObj(md.Object))
202+
}
203+
item.Variables = mdVariables
204+
}
205+
req.Items[i] = item
206+
}
207+
return nil
208+
}
209+
210+
func getMDTopologyFromMD(blueprint *scope.ClusterBlueprint, md *clusterv1.MachineDeployment) (*clusterv1.MachineDeploymentTopology, error) {
211+
topologyName, ok := md.Labels[clusterv1.ClusterTopologyMachineDeploymentNameLabel]
212+
if !ok {
213+
return nil, errors.Errorf("failed to get topology name for %s", klog.KObj(md))
214+
}
215+
mdTopology, err := lookupMDTopology(blueprint.Topology, topologyName)
216+
if err != nil {
217+
return nil, err
218+
}
219+
return mdTopology, nil
220+
}
221+
142222
// createRequest creates a GeneratePatchesRequest based on the ClusterBlueprint and the desired state.
143-
// ClusterBlueprint supplies the templates. Desired state is used to calculate variables which are later used
144-
// as input for the patch generation.
145223
// NOTE: GenerateRequestTemplates are created for the templates of each individual MachineDeployment in the desired
146224
// state. This is necessary because some builtin variables are MachineDeployment specific. For example version and
147225
// replicas of a MachineDeployment.
148226
// NOTE: A single GeneratePatchesRequest object is used to carry templates state across subsequent Generate calls.
227+
// NOTE: This function does not add variables to items for the request, as the variables depend on the specific patch.
149228
func createRequest(blueprint *scope.ClusterBlueprint, desired *scope.ClusterState) (*runtimehooksv1.GeneratePatchesRequest, error) {
150229
req := &runtimehooksv1.GeneratePatchesRequest{}
151230

152-
// Calculate global variables.
153-
globalVariables, err := variables.Global(blueprint.Topology, desired.Cluster)
154-
if err != nil {
155-
return nil, errors.Wrapf(err, "failed to calculate global variables")
156-
}
157-
req.Variables = globalVariables
158-
159231
// Add the InfrastructureClusterTemplate.
160232
t, err := newRequestItemBuilder(blueprint.InfrastructureClusterTemplate).
161233
WithHolder(desired.Cluster, "spec.infrastructureRef").
@@ -166,12 +238,6 @@ func createRequest(blueprint *scope.ClusterBlueprint, desired *scope.ClusterStat
166238
}
167239
req.Items = append(req.Items, *t)
168240

169-
// Calculate controlPlane variables.
170-
controlPlaneVariables, err := variables.ControlPlane(&blueprint.Topology.ControlPlane, desired.ControlPlane.Object, desired.ControlPlane.InfrastructureMachineTemplate)
171-
if err != nil {
172-
return nil, errors.Wrapf(err, "failed to calculate ControlPlane variables")
173-
}
174-
175241
// Add the ControlPlaneTemplate.
176242
t, err = newRequestItemBuilder(blueprint.ControlPlane.Template).
177243
WithHolder(desired.Cluster, "spec.controlPlaneRef").
@@ -180,7 +246,6 @@ func createRequest(blueprint *scope.ClusterBlueprint, desired *scope.ClusterStat
180246
return nil, errors.Wrapf(err, "failed to prepare ControlPlane template %s for patching",
181247
tlog.KObj{Obj: blueprint.ControlPlane.Template})
182248
}
183-
t.Variables = controlPlaneVariables
184249
req.Items = append(req.Items, *t)
185250

186251
// If the clusterClass mandates the controlPlane has infrastructureMachines,
@@ -193,7 +258,6 @@ func createRequest(blueprint *scope.ClusterBlueprint, desired *scope.ClusterStat
193258
return nil, errors.Wrapf(err, "failed to prepare ControlPlane's machine template %s for patching",
194259
tlog.KObj{Obj: blueprint.ControlPlane.InfrastructureMachineTemplate})
195260
}
196-
t.Variables = controlPlaneVariables
197261
req.Items = append(req.Items, *t)
198262
}
199263

@@ -216,12 +280,6 @@ func createRequest(blueprint *scope.ClusterBlueprint, desired *scope.ClusterStat
216280
return nil, errors.Errorf("failed to lookup MachineDeployment class %q in ClusterClass", mdTopology.Class)
217281
}
218282

219-
// Calculate MachineDeployment variables.
220-
mdVariables, err := variables.MachineDeployment(mdTopology, md.Object, md.BootstrapTemplate, md.InfrastructureMachineTemplate)
221-
if err != nil {
222-
return nil, errors.Wrapf(err, "failed to calculate variables for %s", tlog.KObj{Obj: md.Object})
223-
}
224-
225283
// Add the BootstrapTemplate.
226284
t, err := newRequestItemBuilder(mdClass.BootstrapTemplate).
227285
WithHolder(md.Object, "spec.template.spec.bootstrap.configRef").
@@ -230,7 +288,6 @@ func createRequest(blueprint *scope.ClusterBlueprint, desired *scope.ClusterStat
230288
return nil, errors.Wrapf(err, "failed to prepare BootstrapConfig template %s for MachineDeployment topology %s for patching",
231289
tlog.KObj{Obj: mdClass.BootstrapTemplate}, mdTopologyName)
232290
}
233-
t.Variables = mdVariables
234291
req.Items = append(req.Items, *t)
235292

236293
// Add the InfrastructureMachineTemplate.
@@ -241,7 +298,6 @@ func createRequest(blueprint *scope.ClusterBlueprint, desired *scope.ClusterStat
241298
return nil, errors.Wrapf(err, "failed to prepare InfrastructureMachine template %s for MachineDeployment topology %s for patching",
242299
tlog.KObj{Obj: mdClass.InfrastructureMachineTemplate}, mdTopologyName)
243300
}
244-
t.Variables = mdVariables
245301
req.Items = append(req.Items, *t)
246302
}
247303

@@ -431,3 +487,16 @@ func updateDesiredState(ctx context.Context, req *runtimehooksv1.GeneratePatches
431487

432488
return nil
433489
}
490+
491+
// definitionsForPatch returns a set which of variables in a ClusterClass defined by "definitionFrom".
492+
func definitionsForPatch(blueprint *scope.ClusterBlueprint, definitionFrom string) map[string]bool {
493+
variableDefinitionsForPatch := make(map[string]bool)
494+
for _, definitionsWithName := range blueprint.ClusterClass.Status.Variables {
495+
for _, definition := range definitionsWithName.Definitions {
496+
if definition.From == definitionFrom {
497+
variableDefinitionsForPatch[definitionsWithName.Name] = true
498+
}
499+
}
500+
}
501+
return variableDefinitionsForPatch
502+
}

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

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func TestApply(t *testing.T) {
5454
tests := []struct {
5555
name string
5656
patches []clusterv1.ClusterClassPatch
57+
varDefinitions []clusterv1.ClusterClassStatusVariable
5758
externalPatchResponses map[string]runtimehooksv1.ResponseObject
5859
expectedFields expectedFields
5960
wantErr bool
@@ -479,6 +480,109 @@ func TestApply(t *testing.T) {
479480
},
480481
},
481482
},
483+
{
484+
name: "Should correctly apply variables for a given patch definitionFrom",
485+
varDefinitions: []clusterv1.ClusterClassStatusVariable{
486+
{
487+
Name: "default-worker-infra",
488+
Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
489+
{
490+
From: "inline",
491+
},
492+
},
493+
},
494+
{
495+
Name: "infraCluster",
496+
DefinitionsConflict: true,
497+
Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
498+
{
499+
From: "inline",
500+
},
501+
{
502+
From: "not-used-patch",
503+
},
504+
},
505+
},
506+
},
507+
patches: []clusterv1.ClusterClassPatch{
508+
{
509+
Name: "fake-patch1",
510+
Definitions: []clusterv1.PatchDefinition{
511+
{
512+
Selector: clusterv1.PatchSelector{
513+
APIVersion: builder.InfrastructureGroupVersion.String(),
514+
Kind: builder.GenericInfrastructureClusterTemplateKind,
515+
MatchResources: clusterv1.PatchSelectorMatch{
516+
InfrastructureCluster: true,
517+
},
518+
},
519+
JSONPatches: []clusterv1.JSONPatch{
520+
{
521+
Op: "add",
522+
Path: "/spec/template/spec/resource",
523+
ValueFrom: &clusterv1.JSONPatchValue{
524+
Variable: pointer.String("infraCluster"),
525+
},
526+
},
527+
},
528+
},
529+
{
530+
Selector: clusterv1.PatchSelector{
531+
APIVersion: builder.BootstrapGroupVersion.String(),
532+
Kind: builder.GenericBootstrapConfigTemplateKind,
533+
MatchResources: clusterv1.PatchSelectorMatch{
534+
MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
535+
Names: []string{"default-worker"},
536+
},
537+
},
538+
},
539+
JSONPatches: []clusterv1.JSONPatch{
540+
{
541+
Op: "add",
542+
Path: "/spec/template/spec/resource",
543+
ValueFrom: &clusterv1.JSONPatchValue{
544+
Variable: pointer.String("default-worker-infra"),
545+
},
546+
},
547+
},
548+
},
549+
{
550+
Selector: clusterv1.PatchSelector{
551+
APIVersion: builder.InfrastructureGroupVersion.String(),
552+
Kind: builder.GenericInfrastructureMachineTemplateKind,
553+
MatchResources: clusterv1.PatchSelectorMatch{
554+
MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
555+
Names: []string{"default-worker"},
556+
},
557+
},
558+
},
559+
JSONPatches: []clusterv1.JSONPatch{
560+
{
561+
Op: "add",
562+
Path: "/spec/template/spec/resource",
563+
ValueFrom: &clusterv1.JSONPatchValue{
564+
Variable: pointer.String("default-worker-infra"),
565+
},
566+
},
567+
},
568+
},
569+
},
570+
},
571+
},
572+
expectedFields: expectedFields{
573+
infrastructureCluster: map[string]interface{}{
574+
"spec.resource": "value99",
575+
},
576+
machineDeploymentInfrastructureMachineTemplate: map[string]map[string]interface{}{
577+
"default-worker-topo1": {"spec.template.spec.resource": "value1"},
578+
"default-worker-topo2": {"spec.template.spec.resource": "default-worker-topo2"},
579+
},
580+
machineDeploymentBootstrapTemplate: map[string]map[string]interface{}{
581+
"default-worker-topo1": {"spec.template.spec.resource": "value1"},
582+
"default-worker-topo2": {"spec.template.spec.resource": "default-worker-topo2"},
583+
},
584+
},
585+
},
482586
}
483587
for _, tt := range tests {
484588
t.Run(tt.name, func(t *testing.T) {
@@ -521,6 +625,10 @@ func TestApply(t *testing.T) {
521625
// Add the patches.
522626
blueprint.ClusterClass.Spec.Patches = tt.patches
523627
}
628+
if len(tt.varDefinitions) > 0 {
629+
// If there are variable definitions in the test add them to the ClusterClass.
630+
blueprint.ClusterClass.Status.Variables = tt.varDefinitions
631+
}
524632

525633
// Copy the desired objects before applying patches.
526634
expectedCluster := desired.Cluster.DeepCopy()
@@ -631,12 +739,40 @@ func setupTestObjects() (*scope.ClusterBlueprint, *scope.ClusterState) {
631739
ControlPlane: clusterv1.ControlPlaneTopology{
632740
Replicas: pointer.Int32(3),
633741
},
742+
Variables: []clusterv1.ClusterVariable{
743+
{
744+
Name: "infraCluster",
745+
Value: apiextensionsv1.JSON{Raw: []byte(`"value99"`)},
746+
DefinitionFrom: "inline",
747+
},
748+
{
749+
Name: "infraCluster",
750+
// This variable should not be used as it is from "non-used-patch" which is not applied in any test.
751+
Value: apiextensionsv1.JSON{Raw: []byte(`"should-never-be-used"`)},
752+
DefinitionFrom: "not-used-patch",
753+
},
754+
{
755+
Name: "default-worker-infra",
756+
// This value should be overwritten for the default-worker-topo1 MachineDeployment.
757+
Value: apiextensionsv1.JSON{Raw: []byte(`"default-worker-topo2"`)},
758+
DefinitionFrom: "inline",
759+
},
760+
},
634761
Workers: &clusterv1.WorkersTopology{
635762
MachineDeployments: []clusterv1.MachineDeploymentTopology{
636763
{
637764
Metadata: clusterv1.ObjectMeta{},
638765
Class: "default-worker",
639766
Name: "default-worker-topo1",
767+
Variables: &clusterv1.MachineDeploymentVariables{
768+
Overrides: []clusterv1.ClusterVariable{
769+
{
770+
Name: "default-worker-infra",
771+
DefinitionFrom: "inline",
772+
Value: apiextensionsv1.JSON{Raw: []byte(`"value1"`)},
773+
},
774+
},
775+
},
640776
},
641777
{
642778
Metadata: clusterv1.ObjectMeta{},
@@ -697,6 +833,7 @@ func setupTestObjects() (*scope.ClusterBlueprint, *scope.ClusterState) {
697833
MachineDeployments: map[string]*scope.MachineDeploymentState{
698834
"default-worker-topo1": {
699835
Object: builder.MachineDeployment(metav1.NamespaceDefault, "md1").
836+
WithLabels(map[string]string{clusterv1.ClusterTopologyMachineDeploymentNameLabel: "default-worker-topo1"}).
700837
WithVersion("v1.21.2").
701838
Build(),
702839
// Make sure we're using an independent instance of the template.
@@ -705,6 +842,7 @@ func setupTestObjects() (*scope.ClusterBlueprint, *scope.ClusterState) {
705842
},
706843
"default-worker-topo2": {
707844
Object: builder.MachineDeployment(metav1.NamespaceDefault, "md2").
845+
WithLabels(map[string]string{clusterv1.ClusterTopologyMachineDeploymentNameLabel: "default-worker-topo2"}).
708846
WithVersion("v1.20.6").
709847
WithReplicas(5).
710848
Build(),

0 commit comments

Comments
 (0)