Fix preservation reconciliation to prevent deviation from regular flow for non-preservation-bound machines#1111
Fix preservation reconciliation to prevent deviation from regular flow for non-preservation-bound machines#1111thiyyakat wants to merge 19 commits into
Conversation
…eservation state.
…hase. Ignore node not found errors.
…ines is not modified by `manageMachinePreservation`
…ss function calls.
…n manageMachinePreservation: This call is made to uncordon nodes that are preserved with preserve=now and the machine has recovered to Running. Preservation is not stopped in this case.
063f8db to
f8d53be
Compare
There was a problem hiding this comment.
after we changed literal value we should change the name to the constant to also PreserveMachineAnnotationValueAutoPreserved instead. This package will be exposed to consumers like DWD so best to get semantic name changes finalized.
… reduced when possible
…edByMCM` to `PreserveMachineAnnotationValueAutoPreserved`
f8d53be to
200ce00
Compare
| preservationBound := c.isMachinePreservationBound(preserveInfo) | ||
| if !preservationBound { | ||
| // we clear the error here to prevent preservation logic from interfering with non-preservation-bound machines | ||
| err = nil |
There was a problem hiding this comment.
Why do you need to set err to nil here?
I see err gets populated before this only when calling c.getPreserveStateInfo(), and even then if err != nil, we return
There was a problem hiding this comment.
You're right. The earlier return defeats the purpose of this. I will probably need to buffer the error until we can determine whether or not the machine is preservation-bound. Thanks
There was a problem hiding this comment.
Addressed in cb105e7. The error from getPreserveStateInfo is now buffered in getErr and is only returned if machine is preservation-bound.
| } else if err != nil { | ||
| return | ||
| } |
There was a problem hiding this comment.
Similar here. err is populated only by c.getPreserveStateInfo(), and there we return if err != nil
| nodeValue string | ||
| machineValue string | ||
| lastAppliedNodeValue string | ||
| PreserveExpiryTimeSet bool |
There was a problem hiding this comment.
Need not be exported
| PreserveExpiryTimeSet bool | |
| preserveExpiryTimeSet bool |
| _, err := c.clearMachinePreserveExpiryTime(ctx, machine) | ||
| klog.Warningf("Node %q of machine %q not found. Proceeding to stop preservation on machine.", nodeName, machine.Name) | ||
| // Node not found, proceed to delete annotations and clear preserveExpiryTime on machine | ||
| machine, err = c.removePreserveAnnotationOnMachine(ctx, machine) |
There was a problem hiding this comment.
Is there a reason this is not gated with removePreservationAnnotations? I see that every other invocation is removePreserveAnnotationOnMachine() within this function is gated
| // PreventAutoPreserveAnnotationValues contains the values to check if a machine is already annotated for preservation, | ||
| // in which case it should not be auto-preserved. | ||
| var PreventAutoPreserveAnnotationValues = sets.New(PreserveMachineAnnotationValueNow, PreserveMachineAnnotationValueWhenFailed, PreserveMachineAnnotationValuePreservedByMCM, PreserveMachineAnnotationValueFalse) | ||
| var AllowedPreserveAnnotationValues = sets.New(PreserveMachineAnnotationValueNow, PreserveMachineAnnotationValueWhenFailed, PreserveMachineAnnotationValueAutoPreserved, PreserveMachineAnnotationValueFalse) |
There was a problem hiding this comment.
Yes! There is no use for preserve="". The user can either delete the annotation, or set it to false.
| machineAnnotationValue: machineutils.PreserveMachineAnnotationValuePreservedByMCM, | ||
| }, | ||
| }), | ||
| Entry("when node is annotated and preservation times out, should stop preservation", testCase{ |
There was a problem hiding this comment.
Why is this test removed? This looks like a valid case
There was a problem hiding this comment.
The test following this one covers this test case and also checks for annotation removal.
| retry: machineutils.LongRetry, | ||
| }, | ||
| }), | ||
| Entry("when invalid preserve annotation is added on node of un-preserved machine, should do nothing ", testCase{ |
There was a problem hiding this comment.
This looks like a valid case too
There was a problem hiding this comment.
The test following this one is a duplicate of this one.
| return true | ||
| } | ||
|
|
||
| func (c *controller) getPreserveStateInfo(machine *v1alpha1.Machine) (*preserveStateInfo, error) { |
There was a problem hiding this comment.
Please consider writing some unit tests for the new helper methods that you introduced
| if err != nil { | ||
| if apierrors.IsConflict(err) { | ||
| if apierrors.IsNotFound(err) { | ||
| klog.Warningf("Error during preservation flow:%v", err.Error()) |
There was a problem hiding this comment.
nit
| klog.Warningf("Error during preservation flow:%v", err.Error()) | |
| klog.Warningf("Error during preservation flow: %v", err.Error()) |
| if err != nil { | ||
| return err | ||
| } | ||
| func (c *controller) uncordonNodeIfCordoned(ctx context.Context, node *corev1.Node) error { |
There was a problem hiding this comment.
Should there be a check for ClusterAutoscalerScaleDownDisabledAnnotationByMCMKey annotation before node operations, similar to how it is done in removePreservationRelatedAnnotationsOnNode()?
There was a problem hiding this comment.
Do you mean to gate the uncordoning? Do you feel the preservation-bound check and Running phase checks are insufficient?
There was a problem hiding this comment.
I think it might be worth adding, as the more we can limit CA interactions with this the better. However, could ask for a second opinion
There was a problem hiding this comment.
The two may not be related though. If we do what you suggest, then there is a chance that the user has set scale-down-disabled on the node in which case we would never uncordon the node even after it recovers from Failed to Running.
The check in removePreservationRelatedAnnotationsOnNode was done to ensure that we don't clear the scale-down-disabled annotation if it has been set by the user.
We limit CA interaction by :
- By-passing preservation logic if machine is not preservation-bound
- Ensuring CA does not scale down a preserved node. We ensure preserved nodes have the scale-down-disabled annotation and in CA's ForceDeleteNode, we skip nodes if they have the scale-down-disabled annotation.
@aaronfern do you think this is sufficient?
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
- nits - if not found, machine annotation value must be enforced - if machine annotation value is invalid, log and continue assuming it does not exist - gate `removePreserveAnnotationOnMachine` when node is not found
69b9eb6 to
cb105e7
Compare
| // preservation of the machine object. | ||
| effectivePreserveValue := getEffectivePreservationAnnotations(preserveInfo, getErr) | ||
|
|
||
| var removeAnnotations, preservationStopped bool |
There was a problem hiding this comment.
Are these two variables required?
preservationStopped can be removed, as CurrentStatus.PreserveExpiryTime is removed by stopPreservationIfActive, and the check for the time is enough in L853 IMO, but with removeAnnotations it is not as obvious and the changes might be more involved.
There was a problem hiding this comment.
removeAnnotations serves 2 purposes:
- It indicates to stopPreservationIfActive whether the machine and not should be unannotated - in the cases where machine preservation expires, or auto-preserved machine has recovered to Running.
- if annotations have been removed by stopPreservationIfActive, updatePreserveAnnotationOnMachine should not run because it will again set LastAppliedNodePreserveValueAnnotation.
There was a problem hiding this comment.
removeAnnotations should stay as, if it is removed, stopPreservationIfActive will need to actively check this.preservationStopped can be removed (Based on discussion)
| if err != nil { | ||
| return err | ||
| } | ||
| func (c *controller) uncordonNodeIfCordoned(ctx context.Context, node *corev1.Node) error { |
There was a problem hiding this comment.
I think it might be worth adding, as the more we can limit CA interactions with this the better. However, could ask for a second opinion
d2f0d8f to
91b9476
Compare
r4mek
left a comment
There was a problem hiding this comment.
Thanks for the PR! I have posted few comments, some of which are more related to code cleanup which you can choose to do now or defer to a later PR.
| machineValue string | ||
| lastAppliedNodeValue string | ||
| preserveExpiryTimeSet bool | ||
| } |
There was a problem hiding this comment.
Since "" annotation value is not valid anymore, we can remove nodeAnnotated and machineAnnotated booleans. Annotation value is "" implies that annotation is not set.
Also, can we rename nodeValue, machineValue to nodeAnnotationValue, machineAnnotationValue resp.
There was a problem hiding this comment.
The only way to distinguish between node/machine being annotated invalidly with "", or whether the annotation is unset(valid- means deletion of annotation), is by checking nodeAnnotated/machineAnnotated.
| } | ||
| klog.Warningf("Couldn't find node %q for machine %q", nodeName, machine.Name) | ||
| err = nil | ||
| } |
There was a problem hiding this comment.
You can move this check right after isMachinePreservationBound() call.
There was a problem hiding this comment.
We want to check if a machine is preservation-bound at all before we return an error. This is being done so that we don't alter the flow for machines that are not preservation-bound. This is the current issue that the PR is trying to solve.
| delete(clonedMachineAnnotations, machineutils.PreserveMachineAnnotationKey) | ||
| clonedMachineAnnotations[machineutils.LastAppliedNodePreserveValueAnnotationKey] = nodeAnnotationValue | ||
| return nodeAnnotationValue, clonedMachineAnnotations | ||
| if info.nodeValue == "" && info.lastAppliedNodeValue == "" { |
There was a problem hiding this comment.
Why do we have to also check for lastAppliedNodeValue?
There was a problem hiding this comment.
Please check the comment above (sorry, it's long). It gives an example scenario of why lastAppliedNodeValue is needed. If MCM goes down, we won't be able to tell if node was never annotated or if node annotation was deleted.
| // removal of preserveExpiryTime is the last step of stopping preservation | ||
| // therefore, if preserveExpiryTime is not set, machine is not preserved | ||
| nodeName := machine.Labels[v1alpha1.NodeLabelKey] | ||
| if machine.Status.CurrentStatus.PreserveExpiryTime == nil { |
There was a problem hiding this comment.
We have the info.preserveExpiryTimeSet and we already check for it in the parent function. So, no need for this.
If we still want to be check it again then shouldn't we clear the annotations before returning?
There was a problem hiding this comment.
info.preserveExpiryTimeSet is only being used to check if a machine is preservation-bound. We do not pass it to stopPreservationIfActive.
| klog.V(2).Infof("Preservation of machine %q with no backing node has stopped.", machine.Name) | ||
| return true, nil | ||
| return machine, nil | ||
| } |
There was a problem hiding this comment.
We can extract this block outside, so that we always remove preserve annotation from machine and clear clear machine expiry time. Later we can return if nodeName == nil
Then we won't have to do the same operation again if node is NotFound.
There was a problem hiding this comment.
Will raise a separate PR for any refactoring needed. Since the purpose of this is to play it safe and quickly deliver a fix without introducing any new regressions, I would like to leave this as-is for now.
| } | ||
| // Step 2: remove annotations from node | ||
| err = c.removePreservationRelatedAnnotationsOnNode(ctx, updatedNode, removePreservationAnnotations) | ||
| updatedNode, err = c.removePreservationRelatedAnnotationsOnNode(ctx, updatedNode, removePreservationAnnotations) |
There was a problem hiding this comment.
Can we combine this and the condition update on node as a single operation?
There was a problem hiding this comment.
We cannot unfortunately. Annotations are updated using Update and conditions using UpdateStatus.
| } | ||
|
|
||
| // manageMachinePreservation manages machine preservation based on the preserve annotation values on the node and machine objects. | ||
| func (c *controller) manageMachinePreservation(ctx context.Context, machine *v1alpha1.Machine) (retry machineutils.RetryPeriod, err error) { |
There was a problem hiding this comment.
We can split this function such that one handles the case where nodeName is set and other where nodeName is not set.
There was a problem hiding this comment.
Will raise a separate PR for refactoring. Since the purpose of this is to play it safe and quickly deliver a fix without introducing any new regressions, I would like to leave this as-is for now.
| klog.Errorf("error draining preserved node %q for machine %q : %v", nodeName, machine.Name, drainErr) | ||
| return drainErr | ||
| return nil, drainErr | ||
| } |
There was a problem hiding this comment.
I don't think we will reach this codepath as we are gonna skip the if needsUpdate { if we have drainErr. So this block can be shifted after c.drainPreservedNode() call.
Hence, we would be returning without an error even if drain was unsuccessful.
There was a problem hiding this comment.
Why would we skip if needsUpdate{ if we have drainErr that is not nil? In computeNewNodePreservedCondition we set needsUpdate to true if it is the first time that drain is failing.
…encing happens in future edits. Add removed test.
What this PR does / why we need it:
The previous implementation had several correctness issues in the machine preservation flow:
Runningmachine with a cordoned backing node — even one cordoned for reasons unrelated to preservation (e.g. Cluster Autoscaler scale-down) — would be unconditionally uncordoned on every reconcile pass.LastAppliedNodePreserveValueAnnotationsync in the defer compared annotations on a clone that had already been updated via UpdateStatus calls insidepreserveMachine/stopPreservationIfActive. Because UpdateStatus returns the server's stored metadata (not the in-memory mutation), the annotation diff was never detected and the sync was silently dropped. On paths where the sync did fire, it was using a stale ResourceVersion, producing spurious conflict errors.This PR fixes all of the above by:
preserveStateInfo structpopulated bygetPreserveStateInfoat the start of each reconcile, reading node and machine annotation state in a single pass and storing it for the rest of the function. This eliminates the double lister read and decouples the annotation sync from the defer.isMachinePreservationBoundgate: machines with no preservation state (noPreserveExpiryTime, no preserve annotation on machine or node, noLastAppliedNodePreserveValueAnnotation) skip the entire preservation flow andreturn LongRetry, nilimmediately.LastAppliedNodePreserveValueAnnotationsync to an explicitupdatePreserveAnnotationOnMachinecall at the end of the function viashouldAnnotationsBeUpdatedOnMachine, rather than a defer that compared potentially stale annotation maps.stopPreservationIfActive(step 4), so it fires only when preservation is actively being stopped for aRunning machine— not on every reconcile of any active machine. ThemanageMachinePreservationpath retains a narrower uncordon call gated on!preservationStopped && PreserveExpiryTime != nil && Phase == Runningto handle the case wherepreserveMachineis called on aRunningmachine (e.g.preserve=nowset by user).IsNotFoundhandling in the defer so transient not-found errors during preservation (e.g. machine deleted mid-reconcile) returnLongRetryrather thanShortRetry.removePreservationRelatedAnnotationsOnNode(now returns*corev1.Node) so that the subsequent uncordon step instopPreservationIfActiveoperates on a current node object rather than a potentially stale one.PreventAutoPreserveAnnotationValuesset in machineutils (it was identical toAllowedPreserveAnnotationValuesminus "");manageAutoPreservationOfFailedMachinesin machineset.go now usesAllowedPreserveAnnotationValuesdirectly.Which issue(s) this PR fixes:
Fixes #1110
Special notes for your reviewer:
Release note: