Skip to content

Add support for hook to suspend VM deletion - #1158

Open
CaptainIRS wants to merge 9 commits into
gardener:masterfrom
CaptainIRS:suspend-deletion
Open

CaptainIRS wants to merge 9 commits into
gardener:masterfrom
CaptainIRS:suspend-deletion

Conversation

@CaptainIRS

@CaptainIRS CaptainIRS commented Sep 22, 2026

Copy link
Copy Markdown
Member

What this PR does / why we need it:

Adds support for suspending VM deletion after node draining, allowing external controllers to perform pre-termination actions such as removing an etcd member before the VM is deleted.

When a terminating Machine reaches the VM deletion stage and has a suspension annotation, MCM:

  • skips the provider VM deletion;
  • sets the InstanceDeletionSuspended=True condition;
  • reports the responsible owner and purpose in the condition message;

Once the annotation is removed by the external controller, MCM proceeds to start deleting the VM in the reconciliation triggered by the annotation removal.

The annotation format is:

suspend-instance-deletion.node.machine.sapcloud.io/<purpose>: <owner>

For example:

suspend-instance-deletion.node.machine.sapcloud.io/etcd-member-removal: my-controller

Once the external controller removes the annotation, MCM clears the condition and resumes VM deletion.

Which issue(s) this PR fixes:

Fixes #1131

Special notes for your reviewer:

  • No new MachinePhase is introduced; the existing Terminating phase and deletion flow are preserved.
  • The annotation intentionally differs from the format suggested in the issue comments. node.machine.sapcloud.io/suspend-instance-deletion/<purpose> is not a valid Kubernetes annotation key because it contains two / separators. The implemented format uses suspend-instance-deletion.node.machine.sapcloud.io as the DNS prefix.
  • Multiple suspension annotations are supported, with deterministic condition messages.
  • Tests cover annotation parsing, suspension, condition updates, and resuming deletion.
  • Verified with:
    • go test ./pkg/util/annotations
    • go test ./pkg/util/provider/machinecontroller

Testing with g/g:

This patch can be utilized.
diff --git a/example/provider-local/shoot.yaml b/example/provider-local/shoot.yaml
index e5731ed90a..f105440a09 100644
--- a/example/provider-local/shoot.yaml
+++ b/example/provider-local/shoot.yaml
@@ -25,12 +25,14 @@ spec:
     type: local
     workers:
     - name: local
+      annotations:
+        suspend-instance-deletion.node.machine.sapcloud.io/etcd-member-removal: my-controller
       machine:
         type: local
       cri:
         name: containerd
-      minimum: 1
-      maximum: 2
+      minimum: 3
+      maximum: 4
       maxSurge: 1
       maxUnavailable: 0
   kubernetes:
diff --git a/extensions/pkg/controller/worker/genericactuator/actuator_reconcile.go b/extensions/pkg/controller/worker/genericactuator/actuator_reconcile.go
index a9051893f7..6dca6f6973 100644
--- a/extensions/pkg/controller/worker/genericactuator/actuator_reconcile.go
+++ b/extensions/pkg/controller/worker/genericactuator/actuator_reconcile.go
@@ -304,7 +304,8 @@ func deployMachineDeployment(
 		machineDeployment.Spec.AutoPreserveFailedMachineMax = deployment.AutoPreserveFailedMachineMax
 		machineDeployment.Spec.Template = machinev1alpha1.MachineTemplateSpec{
 			ObjectMeta: metav1.ObjectMeta{
-				Labels: getMachineLabels(deployment.Strategy, labels, worker.Name),
+				Labels:      getMachineLabels(deployment.Strategy, labels, worker.Name),
+				Annotations: deployment.Annotations,
 			},
 			Spec: machinev1alpha1.MachineSpec{
 				Class: machinev1alpha1.ClassSpec{
diff --git a/go.mod b/go.mod
index 76f81bb4c2..9c215dd007 100644
--- a/go.mod
+++ b/go.mod
@@ -373,3 +373,5 @@ require (
 	sigs.k8s.io/randfill v1.0.0 // indirect
 	sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
 )
+
+replace github.com/gardener/machine-controller-manager => ../machine-controller-manager
diff --git a/go.sum b/go.sum
index bd6f6f4334..57cb66fee4 100644
--- a/go.sum
+++ b/go.sum
@@ -270,8 +270,6 @@ github.com/gardener/dependency-watchdog v1.8.0 h1:+3FE8sR1V6YM9JsGqX7m9oUjhr1wXh
 github.com/gardener/dependency-watchdog v1.8.0/go.mod h1:jRIyBZ4ySWy+EdQjfLm/N+90vBPdWJ7khiBN7CGQs18=
 github.com/gardener/etcd-druid/api v0.38.2 h1:ltaqC9PV5rd39kj766UW+ZsN7LputyF1JplgcohsRHE=
 github.com/gardener/etcd-druid/api v0.38.2/go.mod h1:HsqEM/h4XuxPC4cFDm20V9TPAUqv0tjtnzds/nZ5p7Y=
-github.com/gardener/machine-controller-manager v0.62.1 h1:TQ4Qs9aaYTqSkqFownIaEAvnrYoNpBMcp7ZMYVEgUWM=
-github.com/gardener/machine-controller-manager v0.62.1/go.mod h1:ICsymXK4VHm7NVr8Mpl6USvYcTZjUNSk6+XaVu/dd74=
 github.com/gardener/pvc-autoscaler v0.3.0 h1:vneqFW7WESecbucng0Ravuz825BzKLI3jSKmiopccrs=
 github.com/gardener/pvc-autoscaler v0.3.0/go.mod h1:GVA7q8qiMi2UD/vcCBVbHEUijZvxVqZ1SGqiwKGHTE4=
 github.com/gardener/terminal-controller-manager v0.39.0 h1:w+aKFvM5k5EobILX9vrxgs6VbA5gd88Gyx/3a5m7nVc=

In local gardener setup:

SKAFFOLD_CACHE_ARTIFACTS=false make kind-up && SKAFFOLD_CACHE_ARTIFACTS=false make gardener-up && KUBECONFIG=dev-setup/kubeconfigs/virtual-garden/kubeconfig kubectl apply -f example/provider-local/shoot.yaml

(This uses MCM checked out to this branch. Make sure skaffold is not using the cached MCM local provider image)

KUBECONFIG=dev-setup/kubeconfigs/virtual-garden/kubeconfig kubectl annotate shoot/local -n garden-local gardener.cloud/operation=rollout-workers=local
# Do the following for the machines in the MCD to let termination proceed.
k annotate --overwrite -n shoot--local--local machine/shoot--local--local-local-z1-<machine> suspend-instance-deletion.node.machine.sapcloud.io/etcd-member-removal-

Release note:

Add support for suspending VM deletion using suspend-instance-deletion.node.machine.sapcloud.io annotations.

Signed-off-by: Rinish Sam <rinish.sam@sap.com>
@CaptainIRS
CaptainIRS requested a review from a team as a code owner September 22, 2026 03:36
@gardener-prow

gardener-prow Bot commented Sep 22, 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 aaronfern 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/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Sep 22, 2026
@CaptainIRS

Copy link
Copy Markdown
Member Author

/kind enhancement

@gardener-prow gardener-prow Bot added kind/enhancement Enhancement, improvement, extension and removed do-not-merge/needs-kind Indicates a PR lacks a `kind/foo` label and requires one. labels Sep 22, 2026

@takoverflow takoverflow 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 PR, just took a cursory glance and had some initial comments PTAL

Comment thread pkg/util/provider/machinecontroller/machine_util.go Outdated
Comment thread pkg/util/provider/machinecontroller/machine_util.go Outdated
Comment thread pkg/util/provider/machinecontroller/machine_util.go Outdated
Comment thread pkg/util/provider/machinecontroller/machine_util.go Outdated
Comment thread pkg/util/provider/machinecontroller/machine_util.go Outdated
Comment thread pkg/util/provider/machinecontroller/machine_util.go Outdated
Comment thread pkg/apis/machine/v1alpha1/constants.go Outdated
Comment thread pkg/apis/machine/v1alpha1/constants.go Outdated
Comment thread pkg/util/annotations/annotations_test.go Outdated
Comment thread pkg/util/provider/machinecontroller/machine_test.go Outdated
Comment thread pkg/util/annotations/annotations.go Outdated
@gardener-prow gardener-prow Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Sep 22, 2026
Signed-off-by: Rinish Sam <rinish.sam@sap.com>
…ables

Signed-off-by: Rinish Sam <rinish.sam@sap.com>
…ad of ShortRetry

Signed-off-by: Rinish Sam <rinish.sam@sap.com>
Comment thread pkg/util/provider/machinecontroller/machine_util.go Outdated
Signed-off-by: Rinish Sam <rinish.sam@sap.com>
@takoverflow

Copy link
Copy Markdown
Member

Can you add an intergration test for this annotation as well
At

ginkgo.Context("deletion", func() {

Which tests machine deletion for a machine with this annotation, waits for condition to be added, then the annotation is removed by the test to allow VM deletion to proceed.

Signed-off-by: Rinish Sam <rinish.sam@sap.com>
@CaptainIRS

Copy link
Copy Markdown
Member Author

I've added integration tests, I've also verified with simulated provider. PTAL, thanks.

logs
Machine controllers test machine resource deletion when machines available should suspend deletion until the suspension annotation is removed
/Users/whatever/go/src/github.com/gardener/machine-controller-manager/pkg/test/integration/common/framework.go:741
  > Enter [BeforeEach] Machine controllers test @ 09/23/26 09:20:50.389
  STEP: Checking machineController process is running @ 09/23/26 09:20:50.389
  STEP: Checking machineControllerManager process is running @ 09/23/26 09:20:50.389
  STEP: Checking nodes in target cluster are healthy @ 09/23/26 09:20:50.389
  < Exit [BeforeEach] Machine controllers test @ 09/23/26 09:20:50.394 (5ms)
  > Enter [It] should suspend deletion until the suspension annotation is removed @ 09/23/26 09:20:50.394
  STEP: Creating a machine for the suspension test @ 09/23/26 09:20:50.394
  STEP: Adding the instance deletion suspension annotation @ 09/23/26 09:20:56.421
  STEP: Deleting the machine @ 09/23/26 09:20:56.434
  STEP: Waiting for InstanceDeletionSuspended=True @ 09/23/26 09:20:56.442
  STEP: Removing the instance deletion suspension annotation @ 09/23/26 09:21:08.473
  STEP: Waiting until the machine object is deleted @ 09/23/26 09:21:08.487
  < Exit [It] should suspend deletion until the suspension annotation is removed @ 09/23/26 09:21:24.529 (34.135s)
• [34.140 seconds]

Comment thread pkg/apis/machine/v1alpha1/constants.go Outdated
Comment thread pkg/util/annotations/annotations.go
Comment thread pkg/test/integration/common/framework.go Outdated

@takoverflow takoverflow 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.

/lgtm

Thanks for the PR!

@gardener-prow gardener-prow Bot added the lgtm Indicates that a PR is ready to be merged. label Sep 23, 2026
@gardener-prow

gardener-prow Bot commented Sep 23, 2026

Copy link
Copy Markdown

LGTM label has been added.

DetailsGit tree hash: c39d6cece40af10e51cd05485a14f10b602dbcef

Signed-off-by: Rinish Sam <rinish.sam@sap.com>
@gardener-prow gardener-prow Bot removed the lgtm Indicates that a PR is ready to be merged. label Sep 23, 2026
Comment thread pkg/util/annotations/annotations.go Outdated
…exceeding the max length

Signed-off-by: Rinish Sam <rinish.sam@sap.com>

@aaronfern aaronfern 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.

Last nit, otherwise lgtm

Comment thread docs/faq.md Outdated
- Long term: Please set more appropriate PDBs which allow disruption of at least one pod.
- Expired cloud credentials can block the deletion of the machine from infrastructure.
- Cloud provider can't delete the machine due to internal errors. Such situations are best debugged by using cloud provider specific CLI or cloud console.
- A controller may intentionally suspend VM deletion by adding an annotation such as `suspend-instance-deletion.node.machine.sapcloud.io/my-reason: my-controller`. Check the `Machine`'s annotations and `.status.conditions` for `type: InstanceDeletionSuspended` with `status: "True"`; remove the suspension annotation once the controller's work is complete so MCM can continue deletion.

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.

nit: just to make it clear that my-reason isn't actually part of the annotation

Suggested change
- A controller may intentionally suspend VM deletion by adding an annotation such as `suspend-instance-deletion.node.machine.sapcloud.io/my-reason: my-controller`. Check the `Machine`'s annotations and `.status.conditions` for `type: InstanceDeletionSuspended` with `status: "True"`; remove the suspension annotation once the controller's work is complete so MCM can continue deletion.
- A controller may intentionally suspend VM deletion by adding an annotation such as `suspend-instance-deletion.node.machine.sapcloud.io/<my-reason>: <my-controller>`. Check the `Machine`'s annotations and `.status.conditions` for `type: InstanceDeletionSuspended` with `status: "True"`; remove the suspension annotation once the controller's work is complete so MCM can continue deletion.

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.

Sure, fixed in 4e95259.

Signed-off-by: Rinish Sam <rinish.sam@sap.com>

@aaronfern aaronfern 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 PR and for making the changes!
/lgtm

@gardener-prow gardener-prow Bot added the lgtm Indicates that a PR is ready to be merged. label Sep 23, 2026
@gardener-prow

gardener-prow Bot commented Sep 23, 2026

Copy link
Copy Markdown

LGTM label has been added.

DetailsGit tree hash: 4cacb8c13a5ff82b04bebce9559f684ff4e06adf

@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 PR!

Expect(actualCondition).ToNot(BeNil())
Expect(actualCondition.Type).To(Equal(v1alpha1.InstanceDeletionSuspended))
Expect(actualCondition.Status).To(Equal(corev1.ConditionTrue))
Expect(actualCondition.Message).To(Equal("Instance Deletion suspended by my-controller for etcd-member-removal."))

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.

Could we abstract my-controller and etcd-member-removal as consts, so that any future tests can use it instead of a string?

}

var suspensions []instanceDeletionSuspension
if owner, exists := machine.Annotations[v1alpha1.AnnotationKeySuspendInstanceDeletionPrefix]; exists {

@gagan16k gagan16k Sep 23, 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.

Could we have validation for owner to be a non empty string? Or else we can get malformed messages like "Instance Deletion suspended by for xxx-reason."

func IsInstanceDeletionSuspended(machine *v1alpha1.Machine) (string, bool) {
suspensions := getInstanceDeletionSuspensions(machine)
if len(suspensions) == 0 {
return "", false

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.

Since we return false only when the returned string is "", do we need the boolean return value?

}
oldSuspensionMessage, oldSuspended := annotationsutils.IsInstanceDeletionSuspended(oldMachine)
newSuspensionMessage, newSuspended := annotationsutils.IsInstanceDeletionSuspended(newMachine)
if (oldSuspended != newSuspended || oldSuspensionMessage != newSuspensionMessage) && c.shouldMachineBeMovedToTerminatingQueue(newMachine) {

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 gate annotation checks on c.shouldMachineBeMovedToTerminatingQueue(newMachine) so that we do not have to do all these things for every machine update?
Future pre delete hooks(if any) can also reuse this block to run their checks only in the case the machine is marked for termination.

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

Labels

kind/enhancement Enhancement, improvement, extension lgtm Indicates that a PR is ready to be merged. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide the ability to perform actions before termination of the VM

5 participants