Repository: kubernetes-sigs/controller-runtime

Issue:
fake client: SSA issues: opts propagation and dryPatch bug
I'm trying to test new SSA support for FakeClient (after [this PR](https://github.com/kubernetes-sigs/controller-runtime/pull/2981)) and I'm facing some `conflict` errors returned by apimachinery; I found out if using FieldManager without setting the FieldOwner option, the manager name will end up being `unknown` and an error similar to `Apply failed with 1 conflict: conflict with "unknown" using {APIVERSION}: {FIELDPATH}` .
After this, I updated my call to fake client's update methods (Patch/Update) to set `client.FieldOwner("something")` but realised that it was not being propagated within Patch method (while it was fine for Update).
[pkg/client/fake/client.go](../tree/e8c5c5445a20af34453aab1194fc7d64464d2150/pkg/client/fake/client.go#L1135)

> https://github.com/kubernetes-sigs/controller-runtime/blob/e8c5c5445a20af34453aab1194fc7d64464d2150/pkg/client/fake/client.go#L1135-L1146

I believe action should be created like this:
```go
	action := testing.NewPatchActionWithOptions(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data, *patchOptions.AsPatchOptions())
```

Something similar happened to CREATE:
> https://github.com/kubernetes-sigs/controller-runtime/blob/e8c5c5445a20af34453aab1194fc7d64464d2150/pkg/client/fake/client.go#L873-L877

expected:
```go
	if err := c.tracker.Create(gvr, obj, accessor.GetNamespace(), *createOptions.AsCreateOptions()); err != nil {
		// The managed fields tracker sets gvk even on errors
		_ = ensureTypeMeta(obj, gvk)
		return err
	}
```

Another thing, dryPatch function has the following logic in switch branch for SSA Apply:

> https://github.com/kubernetes-sigs/controller-runtime/blob/e8c5c5445a20af34453aab1194fc7d64464d2150/pkg/client/fake/client.go#L1264-L1278

What is the reason for the deferred function? At least for me this is not working properly, as it tries to add to the tracker the `obj`, which is the zero instance for the received type after this:
> https://github.com/kubernetes-sigs/controller-runtime/blob/e8c5c5445a20af34453aab1194fc7d64464d2150/pkg/client/fake/client.go#L1226-L1229

This leads to the creation of an object with `Name: "", Namespace: ""` in the tracker; this is not failing but in case the SSA PATCH gets executed again for a different instance of the same type, tracker.Add will fail as it will try to re-add the same `empty` instance, triggering the panic in line 1272

I was able to reproduce this last issue by extending existing test `It("supports server-side apply of a client-go resource via Apply method", func(ctx SpecContext) {` to:
```go
	It("supports server-side apply of a client-go resource via Apply method", func(ctx SpecContext) {
		cl := NewClientBuilder().Build()
		obj := corev1applyconfigurations.
			ConfigMap("foo", "default").
			WithData(map[string]string{"some": "data"})

		obj2 := corev1applyconfigurations.
			ConfigMap("foo2", "default").
			WithData(map[string]string{"some": "data"})

		Expect(cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed())
		Expect(cl.Apply(ctx, obj2, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed())

		cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
		cm2 := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo2", Namespace: "default"}}

		Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed())
		Expect(cm.Data).To(BeComparableTo(map[string]string{"some": "data"}))

		obj.Data = map[string]string{"other": "data"}
		Expect(cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed())

		Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed())
		Expect(cm.Data).To(BeComparableTo(map[string]string{"other": "data"}))

		obj.Data = map[string]string{"other": "data"}
		Expect(cl.Apply(ctx, obj2, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed())
		Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm2), cm2)).To(Succeed())
		Expect(cm2.Data).To(BeComparableTo(map[string]string{"other": "data"}))
	})
```
where basically I add a second configmap and fails in the Apply call:
```log
• [PANICKED] [10.264 seconds]
Fake client [It] supports server-side apply of a client-go resource via Apply method
REDACTED/Documents/git/external/controller-runtime/pkg/client/fake/client_test.go:2673

  [PANICKED] Test Panicked
  In [It] at: REDACTED/Documents/git/external/controller-runtime/pkg/client/fake/client.go:1272 @ 08/04/25 15:29:44.302

  configmaps "" already exists

  Full Stack Trace
    sigs.k8s.io/controller-runtime/pkg/client/fake.dryPatch.func1()
    	REDACTED/Documents/git/external/controller-runtime/pkg/client/fake/client.go:1272 +0x1d8
    sigs.k8s.io/controller-runtime/pkg/client/fake.dryPatch({{{0x140005a49c9, 0x7}, {0x1019e34ee, 0x5}, {{0x0, 0x0}, {0x140005a497a, 0x2}, {0x140005a4a20, 0xa}}, ...}, ...}, ...)
    	REDACTED/Documents/git/external/controller-runtime/pkg/client/fake/client.go:1278 +0xb84
    sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).patch(0x14000a4e090, {0x101fe6740, 0x14001e78350}, {0x101fc69f8, 0x102cd7ce0}, {0x1400064b680, 0x1, 0x1})
    	REDACTED/Documents/git/external/controller-runtime/pkg/client/fake/client.go:1143 +0xd14
    sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Apply(0x14000a4e090, {0x12a30d038, 0x140008ca150}, {0x101fc0720, 0x14001c13170}, {0x14000af3450, 0x1, 0x1})
    	REDACTED/Documents/git/external/controller-runtime/pkg/client/fake/client.go:1069 +0x4c4
    sigs.k8s.io/controller-runtime/pkg/client/fake.init.func2.57({0x101fde6a0, 0x140008ca150})
    	REDACTED/Documents/git/external/controller-runtime/pkg/client/fake/client_test.go:2699 +0x1288

```

Discussion:
@alvaroaleman: @ezeriver94 thanks for the testing and the report, this is highly appreciated. I've opened [a fix](https://github.com/kubernetes-sigs/controller-runtime/pull/3268) for the issues you described, could you validate that it fixes everything? Thanks!@ezeriver94: that was quick 😄 I believe the fix is ok, is working for me. Thanks!

Solution patch:
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index cb3f9726..03de411c 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -23,7 +23,7 @@ jobs:
           - ""
           - tools/setup-envtest
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0
       - name: Calculate go version
         id: vars
         run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT
diff --git a/.github/workflows/ossf-scorecard.yaml b/.github/workflows/ossf-scorecard.yaml
index c15807e9..671dbc88 100644
--- a/.github/workflows/ossf-scorecard.yaml
+++ b/.github/workflows/ossf-scorecard.yaml
@@ -26,7 +26,7 @@ jobs:
 
     steps:
       - name: "Checkout code"
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
+        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0
         with:
           persist-credentials: false
 
diff --git a/.github/workflows/pr-dependabot.yaml b/.github/workflows/pr-dependabot.yaml
index 7a8d9c7e..b51cab0d 100644
--- a/.github/workflows/pr-dependabot.yaml
+++ b/.github/workflows/pr-dependabot.yaml
@@ -19,7 +19,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Check out code
-      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
+      uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0
     - name: Calculate go version
       id: vars
       run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 46c63ea5..353a0a1c 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -17,7 +17,7 @@ jobs:
     - name: Set env
       run:  echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV
     - name: Check out code
-      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
+      uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0
     - name: Calculate go version
       id: vars
       run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 0ea80a37..2168d725 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0
 
       - name: Check if PR title is valid
         env:
diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod
index 25d8edae..a483b393 100644
--- a/examples/scratch-env/go.mod
+++ b/examples/scratch-env/go.mod
@@ -16,7 +16,7 @@ require (
 	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
 	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
 	github.com/fsnotify/fsnotify v1.9.0 // indirect
-	github.com/fxamacker/cbor/v2 v2.8.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
@@ -55,17 +55,17 @@ require (
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/api v0.34.0-beta.0 // indirect
-	k8s.io/apiextensions-apiserver v0.34.0-beta.0 // indirect
-	k8s.io/apimachinery v0.34.0-beta.0 // indirect
-	k8s.io/client-go v0.34.0-beta.0 // indirect
+	k8s.io/api v0.34.0-rc.1 // indirect
+	k8s.io/apiextensions-apiserver v0.34.0-rc.1 // indirect
+	k8s.io/apimachinery v0.34.0-rc.1 // indirect
+	k8s.io/client-go v0.34.0-rc.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
 	k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
 	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
 	sigs.k8s.io/randfill v1.0.0 // indirect
-	sigs.k8s.io/structured-merge-diff/v6 v6.2.0 // indirect
-	sigs.k8s.io/yaml v1.5.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
 )
 
 replace sigs.k8s.io/controller-runtime => ../..
diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum
index c6f1a8fd..417738e8 100644
--- a/examples/scratch-env/go.sum
+++ b/examples/scratch-env/go.sum
@@ -17,8 +17,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT
 github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
 github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
 github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
-github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
-github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
@@ -175,14 +175,14 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.34.0-beta.0 h1:Yok13a6+0GTJYv56+lSNlgRjF46TLUVv+gaijkLdkB4=
-k8s.io/api v0.34.0-beta.0/go.mod h1:OOkMaR1spg7vtjCCQSdxgiRD0Bbp2pcTCBxJtEEb4Pk=
-k8s.io/apiextensions-apiserver v0.34.0-beta.0 h1:zzR2yWEBkfYcjfwM42CwrBwtwLWLBJ/VwYpyP2ldNMI=
-k8s.io/apiextensions-apiserver v0.34.0-beta.0/go.mod h1:lCs/ZFdQjQWkNdEa0CS7qvLAMVDCj3fVXwSmKZ7Fn8E=
-k8s.io/apimachinery v0.34.0-beta.0 h1:C6teSJBCx6ArW7122MM9hQqeGW2w/QQ0lB4x4Z4Iftc=
-k8s.io/apimachinery v0.34.0-beta.0/go.mod h1:TP8uyOuDEOnzGpLOdffo8hPnIjVDljZCxCM/fruV+5M=
-k8s.io/client-go v0.34.0-beta.0 h1:snyuzY74xHIp7qxMYul8QRn1WxPNhs281srSl2CX9nY=
-k8s.io/client-go v0.34.0-beta.0/go.mod h1:KNxQTmxlWq7adFeedQLu+qVkIBTZqhghesFk41YWyBQ=
+k8s.io/api v0.34.0-rc.1 h1:S4iMsAUFx9YBgUqBCrD2N9JrL2m85JdRVtfwVi4O3t4=
+k8s.io/api v0.34.0-rc.1/go.mod h1:bntA2P5s25PXa3bIe29bG45qv3JvSJkfyf1MYMV0V1c=
+k8s.io/apiextensions-apiserver v0.34.0-rc.1 h1:phrqFfBsdTFcLA6HQaNAHOgUDxW0CPToKeQPzwYRb+g=
+k8s.io/apiextensions-apiserver v0.34.0-rc.1/go.mod h1:qQz76oYPULy8agz1PtjIf6+QimpNpXAj4BnO+GGu3oI=
+k8s.io/apimachinery v0.34.0-rc.1 h1:2GL0UZ8BHpUoCFI2jaqoyTMvH9/upz7jfsRBnlHIzVM=
+k8s.io/apimachinery v0.34.0-rc.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/client-go v0.34.0-rc.1 h1:xrjjcJOOgyXrDkxHvEPRH2rFYF6CmMOZCMFV24UYUv8=
+k8s.io/client-go v0.34.0-rc.1/go.mod h1:A1rEDyNrSFIXQ1T9pAmIGLujjUlv/CrXYAApVf4Nl1w=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
@@ -193,7 +193,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np
 sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
 sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
 sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
-sigs.k8s.io/structured-merge-diff/v6 v6.2.0 h1:msyqjP8Nyd5sF3QSmJouFSzcBIdwq4ct8d1/7VSBHIQ=
-sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
-sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
-sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/go.mod b/go.mod
index 9c1bfba8..2bafd0f9 100644
--- a/go.mod
+++ b/go.mod
@@ -22,19 +22,19 @@ require (
 	golang.org/x/sys v0.31.0
 	gomodules.xyz/jsonpatch/v2 v2.4.0
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // Using v4 to match upstream
-	k8s.io/api v0.34.0-beta.0
-	k8s.io/apiextensions-apiserver v0.34.0-beta.0
-	k8s.io/apimachinery v0.34.0-beta.0
-	k8s.io/apiserver v0.34.0-beta.0
-	k8s.io/client-go v0.34.0-beta.0
+	k8s.io/api v0.34.0-rc.1
+	k8s.io/apiextensions-apiserver v0.34.0-rc.1
+	k8s.io/apimachinery v0.34.0-rc.1
+	k8s.io/apiserver v0.34.0-rc.1
+	k8s.io/client-go v0.34.0-rc.1
 	k8s.io/klog/v2 v2.130.1
 	k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
-	sigs.k8s.io/structured-merge-diff/v6 v6.2.0
-	sigs.k8s.io/yaml v1.5.0
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.0
+	sigs.k8s.io/yaml v1.6.0
 )
 
 require (
-	cel.dev/expr v0.23.1 // indirect
+	cel.dev/expr v0.24.0 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -42,14 +42,14 @@ require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/fxamacker/cbor/v2 v2.8.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/google/cel-go v0.25.0 // indirect
+	github.com/google/cel-go v0.26.0 // indirect
 	github.com/google/gnostic-models v0.7.0 // indirect
 	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
 	github.com/google/uuid v1.6.0 // indirect
@@ -95,7 +95,7 @@ require (
 	google.golang.org/protobuf v1.36.5 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/component-base v0.34.0-beta.0 // indirect
+	k8s.io/component-base v0.34.0-rc.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
 	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
diff --git a/go.sum b/go.sum
index b210c7d9..ad007629 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
-cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
+cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
+cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -25,8 +25,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
 github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
-github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
-github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -50,8 +50,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
-github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
+github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
+github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
 github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
 github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -229,18 +229,18 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.34.0-beta.0 h1:Yok13a6+0GTJYv56+lSNlgRjF46TLUVv+gaijkLdkB4=
-k8s.io/api v0.34.0-beta.0/go.mod h1:OOkMaR1spg7vtjCCQSdxgiRD0Bbp2pcTCBxJtEEb4Pk=
-k8s.io/apiextensions-apiserver v0.34.0-beta.0 h1:zzR2yWEBkfYcjfwM42CwrBwtwLWLBJ/VwYpyP2ldNMI=
-k8s.io/apiextensions-apiserver v0.34.0-beta.0/go.mod h1:lCs/ZFdQjQWkNdEa0CS7qvLAMVDCj3fVXwSmKZ7Fn8E=
-k8s.io/apimachinery v0.34.0-beta.0 h1:C6teSJBCx6ArW7122MM9hQqeGW2w/QQ0lB4x4Z4Iftc=
-k8s.io/apimachinery v0.34.0-beta.0/go.mod h1:TP8uyOuDEOnzGpLOdffo8hPnIjVDljZCxCM/fruV+5M=
-k8s.io/apiserver v0.34.0-beta.0 h1:cLhc6un0qtCb/nwGpkj8020yvs3oUxCu4vLLKEGkd4c=
-k8s.io/apiserver v0.34.0-beta.0/go.mod h1:RO2L/h9tBdqlrQGz/ghIGOWzF+aEwCkb13ckUdmLolo=
-k8s.io/client-go v0.34.0-beta.0 h1:snyuzY74xHIp7qxMYul8QRn1WxPNhs281srSl2CX9nY=
-k8s.io/client-go v0.34.0-beta.0/go.mod h1:KNxQTmxlWq7adFeedQLu+qVkIBTZqhghesFk41YWyBQ=
-k8s.io/component-base v0.34.0-beta.0 h1:w2zC7n8lXbOqsC8UQ97JqTneDsy+ThFcbxQnfQ98/TM=
-k8s.io/component-base v0.34.0-beta.0/go.mod h1:KRfhQL9KZm3sywusY38Z2An7ulFDCaL+1juNF4NLhUw=
+k8s.io/api v0.34.0-rc.1 h1:S4iMsAUFx9YBgUqBCrD2N9JrL2m85JdRVtfwVi4O3t4=
+k8s.io/api v0.34.0-rc.1/go.mod h1:bntA2P5s25PXa3bIe29bG45qv3JvSJkfyf1MYMV0V1c=
+k8s.io/apiextensions-apiserver v0.34.0-rc.1 h1:phrqFfBsdTFcLA6HQaNAHOgUDxW0CPToKeQPzwYRb+g=
+k8s.io/apiextensions-apiserver v0.34.0-rc.1/go.mod h1:qQz76oYPULy8agz1PtjIf6+QimpNpXAj4BnO+GGu3oI=
+k8s.io/apimachinery v0.34.0-rc.1 h1:2GL0UZ8BHpUoCFI2jaqoyTMvH9/upz7jfsRBnlHIzVM=
+k8s.io/apimachinery v0.34.0-rc.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/apiserver v0.34.0-rc.1 h1:kryO0zj3JsMbtUy2m6SCJCs4DPeXvOfzkImw1c+5KbA=
+k8s.io/apiserver v0.34.0-rc.1/go.mod h1:/RIEpb2F/QtZJkWobwlQmV43LrmM4+tueFde65buex4=
+k8s.io/client-go v0.34.0-rc.1 h1:xrjjcJOOgyXrDkxHvEPRH2rFYF6CmMOZCMFV24UYUv8=
+k8s.io/client-go v0.34.0-rc.1/go.mod h1:A1rEDyNrSFIXQ1T9pAmIGLujjUlv/CrXYAApVf4Nl1w=
+k8s.io/component-base v0.34.0-rc.1 h1:lgnCyQpX3+Eal5C/gky5+GDmWGNbYHBYclgJ6hbM2lk=
+k8s.io/component-base v0.34.0-rc.1/go.mod h1:LbJMxk4TNqKg3lJSAN+xjoklJRPuWWIOIoQ6qRAqjZE=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
@@ -253,7 +253,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np
 sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
 sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
 sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
-sigs.k8s.io/structured-merge-diff/v6 v6.2.0 h1:msyqjP8Nyd5sF3QSmJouFSzcBIdwq4ct8d1/7VSBHIQ=
-sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
-sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
-sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go
index 648d0d75..02d77387 100644
--- a/pkg/cache/cache.go
+++ b/pkg/cache/cache.go
@@ -172,6 +172,17 @@ type Options struct {
 	// is "done" with an object, and would otherwise not requeue it, i.e., we
 	// recommend the `Reconcile` function return `reconcile.Result{RequeueAfter: t}`,
 	// instead of `reconcile.Result{}`.
+	//
+	// SyncPeriod will trigger update events with the old object being equal to the new
+	// object, except when the cache was out of sync.
+	// If you filter update events like this:
+	// Controller.Watch(
+	//     &source.Kind{Type: v1.MyCustomKind},
+	//     &handler.EnqueueRequestForObject{},
+	//     predicate.Or(predicate.GenerationChangedPredicate{},
+	//         predicate.LabelChangedPredicate{}))
+	// then the SyncPeriod will not trigger a Reconcile call, because the update event
+	// will be ignored by the predicate.
 	SyncPeriod *time.Duration
 
 	// ReaderFailOnMissingInformer configures the cache to return a ErrResourceNotCached error when a user
diff --git a/pkg/certwatcher/certwatcher.go b/pkg/certwatcher/certwatcher.go
index c3232409..1cb11683 100644
--- a/pkg/certwatcher/certwatcher.go
+++ b/pkg/certwatcher/certwatcher.go
@@ -240,3 +240,9 @@ func (cw *CertWatcher) handleEvent(event fsnotify.Event) {
 		log.Error(err, "error re-reading certificate")
 	}
 }
+
+// NeedLeaderElection indicates that the cert-manager
+// does not need leader election.
+func (cw *CertWatcher) NeedLeaderElection() bool {
+	return false
+}
diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go
index 380c728b..0ce5dc9f 100644
--- a/pkg/client/fake/client.go
+++ b/pkg/client/fake/client.go
@@ -41,6 +41,7 @@ import (
 	    https://github.com/kubernetes/kubernetes/pull/120326 (v5.6.0+incompatible
 	    missing a critical fix)
 	*/
+
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 	appsv1 "k8s.io/api/apps/v1"
 	authenticationv1 "k8s.io/api/authentication/v1"
@@ -717,6 +718,10 @@ func (c *fakeClient) List(ctx context.Context, obj client.ObjectList, opts ...cl
 		if err := ensureTypeMeta(o, gvk); err != nil {
 			return err
 		}
+
+		if !c.returnManagedFields {
+			o.(metav1.Object).SetManagedFields(nil)
+		}
 	}
 
 	if listOpts.LabelSelector == nil && listOpts.FieldSelector == nil {
@@ -870,7 +875,7 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie
 	c.trackerWriteLock.Lock()
 	defer c.trackerWriteLock.Unlock()
 
-	if err := c.tracker.Create(gvr, obj, accessor.GetNamespace()); err != nil {
+	if err := c.tracker.Create(gvr, obj, accessor.GetNamespace(), *createOptions.AsCreateOptions()); err != nil {
 		// The managed fields tracker sets gvk even on errors
 		_ = ensureTypeMeta(obj, gvk)
 		return err
@@ -1113,11 +1118,7 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
 		return err
 	}
 
-	data, err := patch.Data(obj)
-	if err != nil {
-		return err
-	}
-
+	var isApplyCreate bool
 	c.trackerWriteLock.Lock()
 	defer c.trackerWriteLock.Unlock()
 	oldObj, err := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName())
@@ -1126,32 +1127,59 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client
 			return err
 		}
 		oldObj = &unstructured.Unstructured{}
+		isApplyCreate = true
 	}
 	oldAccessor, err := meta.Accessor(oldObj)
 	if err != nil {
 		return err
 	}
 
-	// Apply patch without updating object.
-	// To remain in accordance with the behavior of k8s api behavior,
-	// a patch must not allow for changes to the deletionTimestamp of an object.
-	// The reaction() function applies the patch to the object and calls Update(),
-	// whereas dryPatch() replicates this behavior but skips the call to Update().
-	// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
-	// to updating the object.
-	action := testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), data)
-	o, err := dryPatch(action, c.tracker, obj)
-	if err != nil {
-		return err
+	// SSA deletionTimestamp updates are silently ignored
+	if patch.Type() == types.ApplyPatchType && !isApplyCreate {
+		obj.SetDeletionTimestamp(oldAccessor.GetDeletionTimestamp())
 	}
-	newObj, err := meta.Accessor(o)
+
+	data, err := patch.Data(obj)
 	if err != nil {
 		return err
 	}
 
-	// Validate that deletionTimestamp has not been changed
-	if !deletionTimestampEqual(newObj, oldAccessor) {
-		return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable")
+	action := testing.NewPatchActionWithOptions(
+		gvr,
+		accessor.GetNamespace(),
+		accessor.GetName(),
+		patch.Type(),
+		data,
+		*patchOptions.AsPatchOptions(),
+	)
+
+	// Apply is implemented in the tracker and calling it has side-effects
+	// such as bumping RV and updating managedFields timestamps, hence we
+	// can not dry-run it. Luckily, the only validation we use it for
+	// doesn't apply to SSA - Creating objects with non-nil deletionTimestamp
+	// through SSA is possible and updating the deletionTimestamp is valid,
+	// but has no effect.
+	if patch.Type() != types.ApplyPatchType {
+		// Apply patch without updating object.
+		// To remain in accordance with the behavior of k8s api behavior,
+		// a patch must not allow for changes to the deletionTimestamp of an object.
+		// The reaction() function applies the patch to the object and calls Update(),
+		// whereas dryPatch() replicates this behavior but skips the call to Update().
+		// This ensures that the patch may be rejected if a deletionTimestamp is modified, prior
+		// to updating the object.
+		o, err := dryPatch(action, c.tracker)
+		if err != nil {
+			return err
+		}
+		newObj, err := meta.Accessor(o)
+		if err != nil {
+			return err
+		}
+
+		// Validate that deletionTimestamp has not been changed
+		if !deletionTimestampEqual(newObj, oldAccessor) {
+			return fmt.Errorf("rejected patch, metadata.deletionTimestamp immutable")
+		}
 	}
 
 	reaction := testing.ObjectReaction(c.tracker)
@@ -1206,7 +1234,7 @@ func deletionTimestampEqual(newObj metav1.Object, obj metav1.Object) bool {
 // This results in some code duplication, but was found to be a cleaner alternative than unmarshalling and introspecting the patch data
 // and easier than refactoring the k8s client-go method upstream.
 // Duplicate of upstream: https://github.com/kubernetes/client-go/blob/783d0d33626e59d55d52bfd7696b775851f92107/testing/fixture.go#L146-L194
-func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker, newObj runtime.Object) (runtime.Object, error) {
+func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker) (runtime.Object, error) {
 	ns := action.GetNamespace()
 	gvr := action.GetResource()
 
@@ -1262,20 +1290,7 @@ func dryPatch(action testing.PatchActionImpl, tracker testing.ObjectTracker, new
 	case types.ApplyCBORPatchType:
 		return nil, errors.New("apply CBOR patches are not supported in the fake client")
 	case types.ApplyPatchType:
-		// There doesn't seem to be a way to test this without actually applying it as apply is implemented in the tracker.
-		// We have to make sure no reader sees this and we can not handle errors resetting the obj to the original state.
-		defer func() {
-			if unstructured, isUnstructured := obj.(*unstructured.Unstructured); isUnstructured && unstructured.GetKind() == "" {
-				unstructured.SetGroupVersionKind(newObj.GetObjectKind().GroupVersionKind())
-			}
-			if err := tracker.Add(obj); err != nil {
-				panic(err)
-			}
-		}()
-		if err := tracker.Apply(gvr, newObj, ns, action.PatchOptions); err != nil {
-			return nil, err
-		}
-		return tracker.Get(gvr, ns, action.GetName())
+		return nil, errors.New("bug in controller-runtime: should not end up in dryPatch for SSA")
 	default:
 		return nil, fmt.Errorf("%s PatchType is not supported", action.GetPatchType())
 	}



Test patch:
diff --git a/pkg/certwatcher/certwatcher_test.go b/pkg/certwatcher/certwatcher_test.go
index 5579c3fd..9737925a 100644
--- a/pkg/certwatcher/certwatcher_test.go
+++ b/pkg/certwatcher/certwatcher_test.go
@@ -37,6 +37,7 @@ import (
 
 	"sigs.k8s.io/controller-runtime/pkg/certwatcher"
 	"sigs.k8s.io/controller-runtime/pkg/certwatcher/metrics"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
 )
 
 var _ = Describe("CertWatcher", func() {
@@ -92,6 +93,12 @@ var _ = Describe("CertWatcher", func() {
 			}
 		})
 
+		It("should not require LeaderElection", func() {
+			leaderElectionRunnable, isLeaderElectionRunnable := any(watcher).(manager.LeaderElectionRunnable)
+			Expect(isLeaderElectionRunnable).To(BeTrue())
+			Expect(leaderElectionRunnable.NeedLeaderElection()).To(BeFalse())
+		})
+
 		It("should read the initial cert/key", func() {
 			// This test verifies the initial read succeeded. So interval doesn't matter.
 			doneCh := startWatcher(10 * time.Second)
diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go
index 0d1589c2..bad2d78b 100644
--- a/pkg/client/fake/client_test.go
+++ b/pkg/client/fake/client_test.go
@@ -2668,6 +2668,27 @@ var _ = Describe("Fake client", func() {
 		_, exists, err := unstructured.NestedFieldNoCopy(u.Object, "metadata", "managedFields")
 		Expect(err).NotTo(HaveOccurred())
 		Expect(exists).To(BeTrue())
+
+		var list corev1.ConfigMapList
+		Expect(cl.List(ctx, &list)).NotTo(HaveOccurred())
+		for _, item := range list.Items {
+			Expect(item.ManagedFields).NotTo(BeNil())
+		}
+	})
+
+	It("clears managedFields from objects in a list", func(ctx SpecContext) {
+		cl := NewClientBuilder().Build()
+		obj := corev1applyconfigurations.
+			ConfigMap("foo", "default").
+			WithData(map[string]string{"some": "data"})
+
+		Expect(cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed())
+
+		var list corev1.ConfigMapList
+		Expect(cl.List(ctx, &list)).NotTo(HaveOccurred())
+		for _, item := range list.Items {
+			Expect(item.ManagedFields).To(BeNil())
+		}
 	})
 
 	It("supports server-side apply of a client-go resource via Apply method", func(ctx SpecContext) {
@@ -2736,6 +2757,88 @@ var _ = Describe("Fake client", func() {
 		Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"}))
 	})
 
+	It("sets managed fields through all methods", func(ctx SpecContext) {
+		owner := "test-owner"
+		cl := client.WithFieldOwner(
+			NewClientBuilder().WithReturnManagedFields().Build(),
+			owner,
+		)
+
+		obj := &corev1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "foo"},
+			Data:       map[string]string{"method": "create"},
+		}
+		Expect(cl.Create(ctx, obj)).NotTo(HaveOccurred())
+
+		Expect(obj.ManagedFields).NotTo(BeEmpty())
+		for _, f := range obj.ManagedFields {
+			Expect(f.Manager).To(BeEquivalentTo(owner))
+		}
+
+		originalObj := obj.DeepCopy()
+		obj.Data["method"] = "patch"
+		Expect(cl.Patch(ctx, obj, client.MergeFrom(originalObj))).NotTo(HaveOccurred())
+		Expect(obj.ManagedFields).NotTo(BeEmpty())
+		for _, f := range obj.ManagedFields {
+			Expect(f.Manager).To(BeEquivalentTo(owner))
+		}
+
+		obj.Data["method"] = "update"
+		Expect(cl.Update(ctx, obj)).NotTo(HaveOccurred())
+		Expect(obj.ManagedFields).NotTo(BeEmpty())
+		for _, f := range obj.ManagedFields {
+			Expect(f.Manager).To(BeEquivalentTo(owner))
+		}
+	})
+
+	// GH-3267
+	It("Doesn't leave stale data when updating an object through SSA", func(ctx SpecContext) {
+		obj := corev1applyconfigurations.
+			ConfigMap("foo", "default").
+			WithData(map[string]string{"some": "data"})
+
+		cl := NewClientBuilder().Build()
+		Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
+
+		obj.WithData(map[string]string{"bar": "baz"})
+		Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
+		var cms corev1.ConfigMapList
+		Expect(cl.List(ctx, &cms)).NotTo(HaveOccurred())
+		Expect(len(cms.Items)).To(BeEquivalentTo(1))
+	})
+
+	It("allows to set deletionTimestamp on an object during SSA create", func(ctx SpecContext) {
+		now := metav1.Now()
+		obj := corev1applyconfigurations.
+			ConfigMap("foo", "default").
+			WithDeletionTimestamp(now).
+			WithData(map[string]string{"some": "data"})
+
+		cl := NewClientBuilder().Build()
+		Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
+
+		Expect(obj.DeletionTimestamp).To(BeEquivalentTo(&now))
+	})
+
+	It("will silently ignore a deletionTimestamp update through SSA", func(ctx SpecContext) {
+		Skip("the apply logic in the managedFieldObjectTracker seems to override this")
+		now := metav1.Now()
+		obj := corev1applyconfigurations.
+			ConfigMap("foo", "default").
+			WithDeletionTimestamp(now).
+			WithFinalizers("foo.bar").
+			WithData(map[string]string{"some": "data"})
+
+		cl := NewClientBuilder().Build()
+		Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
+		Expect(obj.DeletionTimestamp).To(BeEquivalentTo(&now))
+
+		later := metav1.Time{Time: now.Add(time.Second)}
+		obj.DeletionTimestamp = &later
+		Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred())
+		Expect(*obj.DeletionTimestamp).To(BeEquivalentTo(now))
+	})
+
 	scalableObjs := []client.Object{
 		&appsv1.Deployment{
 			ObjectMeta: metav1.ObjectMeta{
diff --git a/pkg/internal/testing/controlplane/etcd.go b/pkg/internal/testing/controlplane/etcd.go
index c30d2132..98ffe3ac 100644
--- a/pkg/internal/testing/controlplane/etcd.go
+++ b/pkg/internal/testing/controlplane/etcd.go
@@ -159,6 +159,10 @@ func (e *Etcd) setProcessState() error {
 // Stop stops this process gracefully, waits for its termination, and cleans up
 // the DataDir if necessary.
 func (e *Etcd) Stop() error {
+	if e.processState == nil {
+		return nil
+	}
+
 	if e.processState.DirNeedsCleaning {
 		e.DataDir = "" // reset the directory if it was randomly allocated, so that we can safely restart
 	}
diff --git a/tools/setup-envtest/go.mod b/tools/setup-envtest/go.mod
index 08698ed6..e8ec258f 100644
--- a/tools/setup-envtest/go.mod
+++ b/tools/setup-envtest/go.mod
@@ -10,8 +10,8 @@ require (
 	github.com/spf13/afero v1.12.0
 	github.com/spf13/pflag v1.0.6
 	go.uber.org/zap v1.27.0
-	k8s.io/apimachinery v0.34.0-beta.0
-	sigs.k8s.io/yaml v1.5.0
+	k8s.io/apimachinery v0.34.0-rc.1
+	sigs.k8s.io/yaml v1.6.0
 )
 
 require (
diff --git a/tools/setup-envtest/go.sum b/tools/setup-envtest/go.sum
index e785ee09..92ece10d 100644
--- a/tools/setup-envtest/go.sum
+++ b/tools/setup-envtest/go.sum
@@ -46,7 +46,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/apimachinery v0.34.0-beta.0 h1:C6teSJBCx6ArW7122MM9hQqeGW2w/QQ0lB4x4Z4Iftc=
-k8s.io/apimachinery v0.34.0-beta.0/go.mod h1:TP8uyOuDEOnzGpLOdffo8hPnIjVDljZCxCM/fruV+5M=
-sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
-sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+k8s.io/apimachinery v0.34.0-rc.1 h1:2GL0UZ8BHpUoCFI2jaqoyTMvH9/upz7jfsRBnlHIzVM=
+k8s.io/apimachinery v0.34.0-rc.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=