Skip to content

Preservation bugfix to put PreserveExpiryTime whenever machine transitions to Failed phase - #1147

Open
r4mek wants to merge 3 commits into
gardener:masterfrom
r4mek:preserve-bug-3
Open

Preservation bugfix to put PreserveExpiryTime whenever machine transitions to Failed phase#1147
r4mek wants to merge 3 commits into
gardener:masterfrom
r4mek:preserve-bug-3

Conversation

@r4mek

@r4mek r4mek commented Sep 1, 2026

Copy link
Copy Markdown
Member

What this PR does / why we need it:

  • Now we add the PreserveExpiryTime on the machine if the machine has annotation: node.machine.sapcloud.io/preserve: "when-failed" and it transitions to Failed phase.
  • For auto-preservation flow, we add the PreserveExpiryTime on the machine if the failed machine gets the annotation: node.machine.sapcloud.io/preserve: "auto-preserved"
  • Miscellaneous code fix:
    • Ensure objects retrieved from the lister cache are never mutated directly.
    • Machine fetch in UpdateMachineWithRetries() from the lister cache, which may return a stale object.
    • On update failure, requeue the object for the next reconciliation rather than retrying with a potentially stale cached value.

Which issue(s) this PR fixes:
Fixes #

Special notes for your reviewer:

Release note:

Fixed the scenario where a machine to be preserved can for brief moment not have PreserveExpiryTime set.

@r4mek
r4mek requested a review from a team as a code owner September 1, 2026 09:59
@gardener-prow

gardener-prow Bot commented Sep 1, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign takoverflow for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@gardener-prow gardener-prow Bot added do-not-merge/needs-kind Indicates a PR lacks a `kind/foo` label and requires one. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Sep 1, 2026
@r4mek

r4mek commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

/kind bug

@gardener-prow gardener-prow Bot added kind/bug Bug and removed do-not-merge/needs-kind Indicates a PR lacks a `kind/foo` label and requires one. labels Sep 1, 2026

@thiyyakat thiyyakat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes and the refactoring! I haven't looked at the full PR yet. I had one case in mind for which I thought the new code may not behave as expected. Once you address that, I will look at the rest as well.

defer trackers.Stop()
waitForCacheSync(stop, c)
err := c.updateMachineAndMachineDeploymentDeletionAnnotations(context.TODO(), testMachineDeployment)
err := func() error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be a stupid question, but what is the point of the closure here? Can you not simply have just L2361?

Comment on lines +181 to +183
node.Annotations[PreserveMachineAnnotationKey]; ok &&
AllowedPreserveAnnotationValues.Has(val) {
return val, true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please print a warning here if the value is not valid.

Comment on lines +190 to +193
if val, ok :=
machine.Annotations[PreserveMachineAnnotationKey]; ok &&
AllowedPreserveAnnotationValues.Has(val) {
return val, true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please log a warning if the value is not valid.

Comment on lines 783 to 786
preserveAnnotationValue, exists := machineutils.GetPreserveAnnotationValue(node, machine)
if !exists {
return
} else if getErr != nil {
if !apierrors.IsNotFound(getErr) {
err = getErr
return
}
klog.Warningf("Couldn't find node %q for machine %q", nodeName, machine.Name)
err = nil
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please correct me if I'm wrong, but what about the case where preservation was being manipulated by only the machine object's annotations? If a machine was preserved, and the annotation was deleted to indicate that preservation should be stopped, you will end up returning early here instead of stopping preservation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Addressed in the latest commit ab6bf44

@r4mek
r4mek marked this pull request as draft September 3, 2026 09:40
@gardener-prow gardener-prow Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Sep 3, 2026
@r4mek
r4mek marked this pull request as ready for review September 4, 2026 07:19
@gardener-prow gardener-prow Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Sep 4, 2026
@gagan16k
gagan16k requested a review from thiyyakat September 6, 2026 16:26

@gagan16k gagan16k left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes! Have some comments, PTAL

machineDeepCopy.Annotations[machineutils.MarkedForDeletionTime] = tgd.markedMachineDeletionTimes[i]
}
_, err = dc.controlMachineClient.Machines(machine.Namespace).Update(ctx, machineDeepCopy, metav1.UpdateOptions{})
deletionTime := tgd.markedMachineDeletionTimes[i]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this variable deletionTime?

return mcd, err
}
// TODO: not neat. refactor later.
for _, machineList := range machineMap {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we key the machineMap by the UID of the machineSet here, might save a loop
Something like

if controllerRef := metav1.GetControllerOf(updatedMachine); controllerRef != nil {
	if machineList, ok := machineMap[controllerRef.UID]; ok {
		for i := range machineList.Items {

m.Status.CurrentStatus.PreserveExpiryTime = &metav1.Time{Time: metav1.Now().Add(c.safetyOptions.MachinePreserveTimeout.Duration)}
}
return nil
}, true)

@gagan16k gagan16k Sep 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per docs

The PUT and POST verbs on objects MUST ignore the status values, to avoid accidentally overwriting the status in read-modify-write scenarios. A /status subresource MUST be provided to enable system components to update statuses of resources they manage.

On annotating machine (with mcm scaled to zero to avoid it reconciling) with these keys and dummy values

(⎈|garden--aws-ha-external:garden shoot--xxx--demo)➜  machine-controller-manager git:(pr/r4mek/1147) ✗ k patch mc shoot--xxx--demo-worker-cpu-z1-58c68-qjrl2 --type=merge -p '{
  "metadata":{"annotations":{"node.machine.sapcloud.io/preserve":"auto-preserved"}},
  "status":{"currentStatus":{"preserveExpiryTime":"2030-01-01T00:00:00Z"}}
}'
machine.machine.sapcloud.io/shoot--xxx--demo-worker-cpu-z1-58c68-qjrl2 patched

(⎈|garden--aws-ha-external:garden shoot--xxx--demo)➜  machine-controller-manager git:(pr/r4mek/1147) ✗ k get mc shoot--xxx--demo-worker-cpu-z1-58c68-qjrl2 -oyaml | grep "annotations" -A 2
  annotations:
    machinepriority.machine.sapcloud.io: "3"
    node.machine.sapcloud.io/preserve: auto-preserved

(⎈|garden--aws-ha-external:garden shoot--xxx--demo)➜  machine-controller-manager git:(pr/r4mek/1147) ✗ k get mc shoot--xxx--demo-worker-cpu-z1-58c68-qjrl2 -oyaml | grep "preserveExpiryTime"

(⎈|garden--aws-ha-external:garden shoot--xxx--demo)➜  machine-controller-manager git:(pr/r4mek/1147) ✗

As the "/status" subresource is not specified here, it drops the change on it. Maybe we would need two different patches for each change as they target different resource endpoints?.

})
updatedMachine, err := machineutils.PatchMachine(ctx, c.Machines(machine.Namespace), &machine, func(m *v1alpha1.Machine) error {
m.Labels = labelsutil.AddLabel(m.Labels, v1alpha1.DefaultMachineDeploymentUniqueLabelKey, hash)
return nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will force a reconcile instead of retry on returning 409 errors, so the unit test for LDRCBST could flake even more now. Might need to find another way to correct that.

Comment on lines +549 to +553
// Deep-copy to avoid mutating cache objects downstream.
for i, m := range filteredMachines {
filteredMachines[i] = m.DeepCopy()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great change, but I was thinking if we could apply this while creating the filteredMachines slices, in claimMachines().
Argument for: Saves extra work on the slice, and does it in the same loop while the slice is getting constructed.
Argument against: The other callers for the function, do not seem to write to the slice like this reconciler does, and thus do not really need a deep copy. WDYT?

machine.Name,
machineutils.PreserveMachineAnnotationKey,
machine.Annotations[machineutils.PreserveMachineAnnotationKey],
machine.Annotations[v1alpha1.NodeLabelKey],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
machine.Annotations[v1alpha1.NodeLabelKey],
machine.Labels[v1alpha1.NodeLabelKey],

return nil
}

func (c *controller) findEffectivePreserveValue(machine *v1alpha1.Machine) (string, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is redundant with the introduction of GetPreserveAnnotationValue()

Comment on lines +225 to +227
if val, ok :=
node.Annotations[PreserveMachineAnnotationKey]; ok &&
AllowedPreserveAnnotationValues.Has(val) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if val, ok :=
node.Annotations[PreserveMachineAnnotationKey]; ok &&
AllowedPreserveAnnotationValues.Has(val) {
if val, ok := node.Annotations[PreserveMachineAnnotationKey]; ok && AllowedPreserveAnnotationValues.Has(val) {

Not a strong opinion, but I found this kind of confusing to read the first time. But that could be just me, feel free to ignore this comment.

klog.V(4).Infof("Machine %s precondition doesn't hold, skip updating it.", name)
retryErr = nil
}
return machineClient.Patch(ctx, machine.Name, types.MergePatchType, patch, metav1.PatchOptions{}, subresources...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return machineClient.Patch(ctx, machine.Name, types.MergePatchType, patch, metav1.PatchOptions{}, subresources...)
return machineClient.Patch(ctx, machine.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}, subresources...)

patch, err := strategicpatch.CreateTwoWayMergePatch(base, modifiedJSON, v1alpha1.Machine{})
if err != nil {
return nil, err
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}
if string(patch) == "{}" {
return machine, nil
}

We could short-circuit if the patch is empty for whatever reason, as the request will still be considered valid by the API server

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/bug Bug size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants