Skip to content

Commit 15615f6

Browse files
Add Taint during rolling update (#10223)
Co-authored-by: Hiromu Asahina <hiromu.a5a@gmail.com>
1 parent 7f4de50 commit 15615f6

File tree

8 files changed

+609
-5
lines changed

8 files changed

+609
-5
lines changed

api/v1beta1/common_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,14 @@ const (
231231
MachineSetPreflightCheckControlPlaneIsStable MachineSetPreflightCheck = "ControlPlaneIsStable"
232232
)
233233

234+
// NodeOutdatedRevisionTaint can be added to Nodes at rolling updates in general triggered by updating MachineDeployment
235+
// This taint is used to prevent unnecessary pod churn, i.e., as the first node is drained, pods previously running on
236+
// that node are scheduled onto nodes who have yet to be replaced, but will be torn down soon.
237+
var NodeOutdatedRevisionTaint = corev1.Taint{
238+
Key: "node.cluster.x-k8s.io/outdated-revision",
239+
Effect: corev1.TaintEffectPreferNoSchedule,
240+
}
241+
234242
// NodeUninitializedTaint can be added to Nodes at creation by the bootstrap provider, e.g. the
235243
// KubeadmBootstrap provider will add the taint.
236244
// This taint is used to prevent workloads to be scheduled on Nodes before the node is initialized by Cluster API.

internal/controllers/machine/machine_controller.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
100100
if err != nil {
101101
return err
102102
}
103+
msToMachines, err := util.MachineSetToObjectsMapper(mgr.GetClient(), &clusterv1.MachineList{}, mgr.GetScheme())
104+
if err != nil {
105+
return err
106+
}
107+
mdToMachines, err := util.MachineDeploymentToObjectsMapper(mgr.GetClient(), &clusterv1.MachineList{}, mgr.GetScheme())
108+
if err != nil {
109+
return err
110+
}
103111

104112
if r.nodeDeletionRetryTimeout.Nanoseconds() == 0 {
105113
r.nodeDeletionRetryTimeout = 10 * time.Second
@@ -122,6 +130,14 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
122130
predicates.ResourceHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue),
123131
),
124132
)).
133+
Watches(
134+
&clusterv1.MachineSet{},
135+
handler.EnqueueRequestsFromMapFunc(msToMachines),
136+
).
137+
Watches(
138+
&clusterv1.MachineDeployment{},
139+
handler.EnqueueRequestsFromMapFunc(mdToMachines),
140+
).
125141
Build(r)
126142
if err != nil {
127143
return errors.Wrap(err, "failed setting up with a controller manager")

internal/controllers/machine/machine_controller_noderef.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ import (
2525
corev1 "k8s.io/api/core/v1"
2626
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2727
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
2829
"k8s.io/klog/v2"
2930
ctrl "sigs.k8s.io/controller-runtime"
3031
"sigs.k8s.io/controller-runtime/pkg/client"
3132

3233
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3334
"sigs.k8s.io/cluster-api/api/v1beta1/index"
35+
"sigs.k8s.io/cluster-api/internal/controllers/machinedeployment/mdutil"
3436
"sigs.k8s.io/cluster-api/internal/util/taints"
3537
"sigs.k8s.io/cluster-api/util"
3638
"sigs.k8s.io/cluster-api/util/annotations"
@@ -133,7 +135,7 @@ func (r *Reconciler) reconcileNode(ctx context.Context, s *scope) (ctrl.Result,
133135
_, nodeHadInterruptibleLabel := node.Labels[clusterv1.InterruptibleLabel]
134136

135137
// Reconcile node taints
136-
if err := r.patchNode(ctx, remoteClient, node, nodeLabels, nodeAnnotations); err != nil {
138+
if err := r.patchNode(ctx, remoteClient, node, nodeLabels, nodeAnnotations, machine); err != nil {
137139
return ctrl.Result{}, errors.Wrapf(err, "failed to reconcile Node %s", klog.KObj(node))
138140
}
139141
if !nodeHadInterruptibleLabel && interruptible {
@@ -255,7 +257,7 @@ func (r *Reconciler) getNode(ctx context.Context, c client.Reader, providerID st
255257

256258
// PatchNode is required to workaround an issue on Node.Status.Address which is incorrectly annotated as patchStrategy=merge
257259
// and this causes SSA patch to fail in case there are two addresses with the same key https://github.com/kubernetes-sigs/cluster-api/issues/8417
258-
func (r *Reconciler) patchNode(ctx context.Context, remoteClient client.Client, node *corev1.Node, newLabels, newAnnotations map[string]string) error {
260+
func (r *Reconciler) patchNode(ctx context.Context, remoteClient client.Client, node *corev1.Node, newLabels, newAnnotations map[string]string, m *clusterv1.Machine) error {
259261
newNode := node.DeepCopy()
260262

261263
// Adds the annotations CAPI sets on the node.
@@ -292,9 +294,70 @@ func (r *Reconciler) patchNode(ctx context.Context, remoteClient client.Client,
292294
// Drop the NodeUninitializedTaint taint on the node given that we are reconciling labels.
293295
hasTaintChanges := taints.RemoveNodeTaint(newNode, clusterv1.NodeUninitializedTaint)
294296

297+
// Set Taint to a node in an old MachineSet and unset Taint from a node in a new MachineSet
298+
isOutdated, err := shouldNodeHaveOutdatedTaint(ctx, r.Client, m)
299+
if err != nil {
300+
return errors.Wrapf(err, "failed to check if Node %s is outdated", klog.KRef("", node.Name))
301+
}
302+
if isOutdated {
303+
hasTaintChanges = taints.EnsureNodeTaint(newNode, clusterv1.NodeOutdatedRevisionTaint) || hasTaintChanges
304+
} else {
305+
hasTaintChanges = taints.RemoveNodeTaint(newNode, clusterv1.NodeOutdatedRevisionTaint) || hasTaintChanges
306+
}
307+
295308
if !hasAnnotationChanges && !hasLabelChanges && !hasTaintChanges {
296309
return nil
297310
}
298311

299312
return remoteClient.Patch(ctx, newNode, client.StrategicMergeFrom(node))
300313
}
314+
315+
func shouldNodeHaveOutdatedTaint(ctx context.Context, c client.Client, m *clusterv1.Machine) (bool, error) {
316+
if _, hasLabel := m.Labels[clusterv1.MachineDeploymentNameLabel]; !hasLabel {
317+
return false, nil
318+
}
319+
320+
// Resolve the MachineSet name via owner references because the label value
321+
// could also be a hash.
322+
objKey, err := getOwnerMachineSetObjectKey(m.ObjectMeta)
323+
if err != nil {
324+
return false, err
325+
}
326+
ms := &clusterv1.MachineSet{}
327+
if err := c.Get(ctx, *objKey, ms); err != nil {
328+
return false, err
329+
}
330+
md := &clusterv1.MachineDeployment{}
331+
objKey = &client.ObjectKey{
332+
Namespace: m.ObjectMeta.Namespace,
333+
Name: m.Labels[clusterv1.MachineDeploymentNameLabel],
334+
}
335+
if err := c.Get(ctx, *objKey, md); err != nil {
336+
return false, err
337+
}
338+
msRev, err := mdutil.Revision(ms)
339+
if err != nil {
340+
return false, err
341+
}
342+
mdRev, err := mdutil.Revision(md)
343+
if err != nil {
344+
return false, err
345+
}
346+
if msRev < mdRev {
347+
return true, nil
348+
}
349+
return false, nil
350+
}
351+
352+
func getOwnerMachineSetObjectKey(obj metav1.ObjectMeta) (*client.ObjectKey, error) {
353+
for _, ref := range obj.GetOwnerReferences() {
354+
gv, err := schema.ParseGroupVersion(ref.APIVersion)
355+
if err != nil {
356+
return nil, err
357+
}
358+
if ref.Kind == "MachineSet" && gv.Group == clusterv1.GroupVersion.Group {
359+
return &client.ObjectKey{Namespace: obj.Namespace, Name: ref.Name}, nil
360+
}
361+
}
362+
return nil, errors.Errorf("failed to find MachineSet owner reference for Machine %s", klog.KRef(obj.GetNamespace(), obj.GetName()))
363+
}

0 commit comments

Comments
 (0)