-
Notifications
You must be signed in to change notification settings - Fork 567
CNTRLPLANE-400: feat(remediationAllowed in NP): propagate MHC RemediationAllowed to NodePool Ready condition #9019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,8 @@ import ( | |
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/client/apiutil" | ||
| "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" | ||
| "sigs.k8s.io/controller-runtime/pkg/event" | ||
| "sigs.k8s.io/controller-runtime/pkg/predicate" | ||
|
|
||
| "github.com/go-logr/logr" | ||
| ) | ||
|
|
@@ -185,6 +187,20 @@ func (c *CAPI) Reconcile(ctx context.Context) error { | |
| Reason: hyperv1.AsExpectedReason, | ||
| ObservedGeneration: nodePool.Generation, | ||
| }) | ||
|
|
||
| // When MHC RemediationAllowed is False, override the Ready condition to signal | ||
| // that auto-repair is blocked because too many machines are unhealthy. | ||
| if remediationAllowed := findMHCRemediationAllowedCondition(mhc.Status.Conditions); remediationAllowed != nil { | ||
| if remediationAllowed.Status == corev1.ConditionFalse { | ||
| SetStatusCondition(&nodePool.Status.Conditions, hyperv1.NodePoolCondition{ | ||
| Type: hyperv1.NodePoolReadyConditionType, | ||
| Status: corev1.ConditionFalse, | ||
| Reason: remediationAllowed.Reason, | ||
| Message: remediationAllowed.Message, | ||
| ObservedGeneration: nodePool.Generation, | ||
| }) | ||
| } | ||
| } | ||
| } else { | ||
| err := c.Get(ctx, client.ObjectKeyFromObject(mhc), mhc) | ||
| if err != nil && !apierrors.IsNotFound(err) { | ||
|
|
@@ -736,6 +752,13 @@ func (c *CAPI) reconcileMachineHealthCheck(ctx context.Context, | |
| } | ||
| } | ||
|
|
||
| // Set the nodePoolAnnotation so the enqueueParentNodePool watch handler | ||
| // can map MHC changes back to the parent NodePool. | ||
| if mhc.Annotations == nil { | ||
| mhc.Annotations = map[string]string{} | ||
| } | ||
| mhc.Annotations[nodePoolAnnotation] = client.ObjectKeyFromObject(nodePool).String() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Even if |
||
|
|
||
| resourcesName := generateName(capiClusterName, nodePool.Spec.ClusterName, nodePool.GetName()) | ||
| mhc.Spec = capiv1.MachineHealthCheckSpec{ | ||
| ClusterName: capiClusterName, | ||
|
|
@@ -1178,6 +1201,15 @@ func (c *CAPI) spotMachineHealthCheck() *capiv1.MachineHealthCheck { | |
| // reconcileSpotMachineHealthCheck reconciles a MachineHealthCheck specifically for spot instances. | ||
| // This MHC selects machines with the interruptibleInstanceLabel. | ||
| func (c *CAPI) reconcileSpotMachineHealthCheck(_ context.Context, mhc *capiv1.MachineHealthCheck) error { | ||
| nodePool := c.nodePool | ||
|
|
||
| // Set the nodePoolAnnotation so the enqueueParentNodePool watch handler | ||
| // can map MHC changes back to the parent NodePool. | ||
| if mhc.Annotations == nil { | ||
| mhc.Annotations = map[string]string{} | ||
| } | ||
| mhc.Annotations[nodePoolAnnotation] = client.ObjectKeyFromObject(nodePool).String() | ||
|
|
||
| // Spot instances need shorter timeouts for faster response to interruption | ||
| maxUnhealthy := intstr.FromString("100%") | ||
| timeOut := 8 * time.Minute | ||
|
|
@@ -1405,3 +1437,44 @@ func (r *NodePoolReconciler) getMachinesForNodePool(ctx context.Context, nodePoo | |
|
|
||
| return sortedByCreationTimestamp(machinesForNodePool), nil | ||
| } | ||
|
|
||
| // findMHCRemediationAllowedCondition finds the RemediationAllowed condition in a CAPI Conditions slice. | ||
| func findMHCRemediationAllowedCondition(conditions capiv1.Conditions) *capiv1.Condition { | ||
| for i := range conditions { | ||
| if conditions[i].Type == capiv1.RemediationAllowedCondition { | ||
| return &conditions[i] | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // mhcRemediationAllowedChangedPredicate returns a predicate that filters MHC events | ||
| // to only pass through when the RemediationAllowed condition has changed. Create and | ||
| // Delete events always pass through; Update events are filtered to avoid unnecessary | ||
| // NodePool reconciliations from unrelated MHC status field changes (e.g. CurrentHealthy, | ||
| // Targets) that are already covered by MachineDeployment/MachineSet/Machine watches. | ||
| func mhcRemediationAllowedChangedPredicate() predicate.Funcs { | ||
| return predicate.Funcs{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: The comment says "Create and Delete events always pass through" but that's implicit (unset
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I"m not totally positive about this but here is my claude answer:
I'm not fixing this |
||
| UpdateFunc: func(e event.UpdateEvent) bool { | ||
| oldMHC, ok := e.ObjectOld.(*capiv1.MachineHealthCheck) | ||
| if !ok { | ||
| return true | ||
| } | ||
| newMHC, ok := e.ObjectNew.(*capiv1.MachineHealthCheck) | ||
| if !ok { | ||
| return true | ||
| } | ||
| oldCond := findMHCRemediationAllowedCondition(oldMHC.Status.Conditions) | ||
| newCond := findMHCRemediationAllowedCondition(newMHC.Status.Conditions) | ||
| if oldCond == nil && newCond == nil { | ||
| return false | ||
| } | ||
| if oldCond == nil || newCond == nil { | ||
| return true | ||
| } | ||
| return oldCond.Status != newCond.Status || | ||
| oldCond.Reason != newCond.Reason || | ||
| oldCond.Message != newCond.Message | ||
| }, | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Should this be a dedicated condition type instead of overriding Ready?
Today
Readyis documented as "bubbles up CAPI MachineDeployment/MachineSet Ready condition — true when all replicas are ready Nodes" (nodepool_conditions.go:50-53). After this change,Ready=Falsecould also mean "nodes are fine but the MHC circuit breaker tripped." An operator seeingReady=False, Reason=TooManyUnhealthycan't easily distinguish an infrastructure failure from a remediation threshold breach.The existing pattern is one condition per signal:
AutorepairEnabled,AllMachinesReady,AllNodesHealthy,UpdatingVersion. Something likeRemediationPausedwould follow that pattern and keep Ready's contract intact.If overriding Ready is the intended approach, the doc comment at
nodepool_conditions.go:50should be updated to reflect this new source.Wdyt @sdminonne?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we go for a separate condition, please not that there are two separate conditions on MachineHealthCheck:
RemediationAllowed and Paused. I would probably choose MachineRemediationAllowed because it's clear it's related to machines and reflects the right condition RemediationAllowed. Using RemediationPaused could confuse this with the other condition "Paused".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that I took the override-readyCondition path 'cause @enxebre pushed for that path in some slack chat (but I cannot find it anymore so let's take it's my choice (and fault).
Overriding 'Ready' is intentional: When the MHC circuit breaker fires, the NodePool is
notReady. Not ready nodes won't be replaced and the pool is degraded so I would saynotReadyit's the right semantic.I knot that we're adding other signals to
Readybut this is already the case since it already aggregates multiple signals: it bubbles upMachineDeployment/MachineSetreadiness, which itself reflects infrastructure provider status, node readiness, and replica counts. Adding one more signal source (MHC circuit breaker) is just another one: same pattern.The existing per-signal conditions (
AutorepairEnabled,AllMachinesReady,AllNodesHealthy) exist to provide drill-down detail whenReadyis false — they don't replaceReady. An operator seeingReady=F alse, Reason=TooManyUnhealthygets an immediately actionable signal; they can then inspectAllMachinesReadyandAllNodesHealthyfor details.But the doc in
nodepool_conditions.gois updated as requested by @jparrill