From 7bc4d1ee993cc269521ef69f612ebeee3f3668fb Mon Sep 17 00:00:00 2001 From: s-z-z Date: Mon, 1 Sep 2025 21:08:49 +0800 Subject: [PATCH 01/68] fix: typo Signed-off-by: s-z-z --- pkg/builder/controller_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/builder/controller_test.go b/pkg/builder/controller_test.go index b1c9c3de3b..46e937d590 100644 --- a/pkg/builder/controller_test.go +++ b/pkg/builder/controller_test.go @@ -148,7 +148,7 @@ var _ = Describe("application", func() { Expect(instance).To(BeNil()) }) - It("should allow creating a controllerw without calling For", func() { + It("should allow creating a controller without calling For", func() { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) From 7ac990f3db92dbef89c20a1ce77fb9adf7aa53eb Mon Sep 17 00:00:00 2001 From: haoqixu Date: Tue, 2 Sep 2025 16:47:18 +0800 Subject: [PATCH 02/68] pkg/client/config: remove outdated doc comments Signed-off-by: haoqixu --- pkg/client/config/config.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/client/config/config.go b/pkg/client/config/config.go index 70389dfa90..1c39f4d854 100644 --- a/pkg/client/config/config.go +++ b/pkg/client/config/config.go @@ -64,9 +64,6 @@ func RegisterFlags(fs *flag.FlagSet) { // The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and // fairness. Set its QPS to a value equal or bigger than 0 to re-enable it. // -// It also applies saner defaults for QPS and burst based on the Kubernetes -// controller manager defaults (20 QPS, 30 burst) -// // Config precedence: // // * --kubeconfig flag pointing at a file @@ -87,9 +84,6 @@ func GetConfig() (*rest.Config, error) { // The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and // fairness. Set its QPS to a value equal or bigger than 0 to re-enable it. // -// It also applies saner defaults for QPS and burst based on the Kubernetes -// controller manager defaults (20 QPS, 30 burst) -// // Config precedence: // // * --kubeconfig flag pointing at a file From 6e1e8b2ee21a1c34989837047f0b84ca071700c7 Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Fri, 5 Sep 2025 10:02:21 +0200 Subject: [PATCH 03/68] Revert deprecation of client.Apply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- pkg/client/fake/client_test.go | 18 +++++++++--------- pkg/client/patch.go | 5 ++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index beb8d38433..72c20fd56f 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -2618,7 +2618,7 @@ var _ = Describe("Fake client", func() { obj.SetName("foo") Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} @@ -2626,7 +2626,7 @@ var _ = Describe("Fake client", func() { Expect(cm.Data).To(Equal(map[string]string{"some": "data"})) Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) Expect(cm.Data).To(Equal(map[string]string{"other": "data"})) @@ -2642,13 +2642,13 @@ var _ = Describe("Fake client", func() { Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "spec")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) Expect(result.Object["spec"]).To(Equal(map[string]any{"some": "data"})) Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "spec")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) @@ -2662,9 +2662,9 @@ var _ = Describe("Fake client", func() { obj.SetName("foo") Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) - err := cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo")) //nolint:staticcheck // will be removed once client.Apply is removed + err := cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo")) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("metadata.managedFields must be nil")) }) @@ -2680,7 +2680,7 @@ var _ = Describe("Fake client", func() { Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} @@ -2688,7 +2688,7 @@ var _ = Describe("Fake client", func() { Expect(cm.Data).To(Equal(map[string]string{"some": "data"})) Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) Expect(cm.Data).To(Equal(map[string]string{"other": "data"})) @@ -2734,7 +2734,7 @@ var _ = Describe("Fake client", func() { "ssa": "value", }, }} - Expect(cl.Patch(ctx, u, client.Apply, client.FieldOwner("foo"))).NotTo(HaveOccurred()) //nolint:staticcheck // will be removed once client.Apply is removed + Expect(cl.Patch(ctx, u, client.Apply, client.FieldOwner("foo"))).NotTo(HaveOccurred()) _, exists, err := unstructured.NestedFieldNoCopy(u.Object, "metadata", "managedFields") Expect(err).NotTo(HaveOccurred()) Expect(exists).To(BeTrue()) diff --git a/pkg/client/patch.go b/pkg/client/patch.go index ec55861080..b99d7663bd 100644 --- a/pkg/client/patch.go +++ b/pkg/client/patch.go @@ -28,7 +28,10 @@ import ( var ( // Apply uses server-side apply to patch the given object. // - // Deprecated: Use client.Client.Apply() instead. + // This should now only be used to patch sub resources, e.g. with client.Client.Status().Patch(). + // Use client.Client.Apply() instead of client.Client.Patch(..., client.Apply, ...) + // This will be deprecated once the Apply method has been added for sub resources. + // See the following issue for more details: https://github.com/kubernetes-sigs/controller-runtime/issues/3183 Apply Patch = applyPatch{} // Merge uses the raw object as a merge patch, without modifications. From a5894805dce56522d851fdbe5b97cf20dfcdcbbd Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 5 Sep 2025 15:06:25 -0400 Subject: [PATCH 04/68] :warning: Fakeclient: Set ResourceVersion for SSA Create We already set this for normal create operations, but missed to do it for creations that happen through SSA. --- pkg/client/fake/client.go | 12 +++++++++--- pkg/client/fake/client_test.go | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 45f9e00e18..4109b5c5eb 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -1171,9 +1171,15 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client return err } - // SSA deletionTimestamp updates are silently ignored - if patch.Type() == types.ApplyPatchType && !isApplyCreate { - obj.SetDeletionTimestamp(oldAccessor.GetDeletionTimestamp()) + if patch.Type() == types.ApplyPatchType { + if isApplyCreate { + // Overwrite it unconditionally, this matches the apiserver behavior + // which allows to set it on create, but will then ignore it. + obj.SetResourceVersion("1") + } else { + // SSA deletionTimestamp updates are silently ignored + obj.SetDeletionTimestamp(oldAccessor.GetDeletionTimestamp()) + } } data, err := patch.Data(obj) diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 72c20fd56f..328dfeecf0 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -2877,6 +2877,29 @@ var _ = Describe("Fake client", func() { Expect(len(cms.Items)).To(BeEquivalentTo(1)) }) + It("sets resourceVersion on SSA create", 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()) + // Ideally we should only test for it to not be empty, realistically we will + // break ppl if we ever start setting a different value. + Expect(obj.ResourceVersion).To(BeEquivalentTo(ptr.To("1"))) + }) + + It("ignores a passed resourceVersion on SSA create", func(ctx SpecContext) { + obj := corev1applyconfigurations. + ConfigMap("foo", "default"). + WithData(map[string]string{"some": "data"}). + WithResourceVersion("1234") + + cl := NewClientBuilder().Build() + Expect(cl.Apply(ctx, obj, client.FieldOwner("foo"))).NotTo(HaveOccurred()) + Expect(obj.ResourceVersion).To(BeEquivalentTo(ptr.To("1"))) + }) + It("allows to set deletionTimestamp on an object during SSA create", func(ctx SpecContext) { now := metav1.Time{Time: time.Now().Round(time.Second)} obj := corev1applyconfigurations. From df962e2739c0c46e90446c675f38318b81e4e932 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:17:16 +0000 Subject: [PATCH 05/68] :seedling: Bump the all-github-actions group with 3 updates Bumps the all-github-actions group with 3 updates: [actions/setup-go](https://github.com/actions/setup-go), [actions/github-script](https://github.com/actions/github-script) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `actions/setup-go` from 5.5.0 to 6.0.0 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/d35c59abb061a4a6fb18e82ac0862c26744d6ab5...44694675825211faa026b3c33043df3e48a5fa00) Updates `actions/github-script` from 7.0.1 to 8.0.0 - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/60a0d83039c74a4aee543508d2ffcb1c3799cdea...ed597411d8f924073f98dfc5c65a23a2325f34cd) Updates `softprops/action-gh-release` from 2.3.2 to 2.3.3 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/72f2c25fcb47643c292f7107632f7a47c1df5cd8...6cbd405e2c4e67a21c47fa9e383d020e4e28b836) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-github-actions - dependency-name: actions/github-script dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-github-actions - dependency-name: softprops/action-gh-release dependency-version: 2.3.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/pr-dependabot.yaml | 2 +- .github/workflows/pr-gh-workflow-approve.yaml | 2 +- .github/workflows/release.yaml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 03de411cce..23284914fd 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -28,7 +28,7 @@ jobs: id: vars run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # tag=v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # tag=v6.0.0 with: go-version: ${{ steps.vars.outputs.go_version }} - name: golangci-lint diff --git a/.github/workflows/pr-dependabot.yaml b/.github/workflows/pr-dependabot.yaml index b51cab0d8c..10162e9129 100644 --- a/.github/workflows/pr-dependabot.yaml +++ b/.github/workflows/pr-dependabot.yaml @@ -24,7 +24,7 @@ jobs: id: vars run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # tag=v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # tag=v6.0.0 with: go-version: ${{ steps.vars.outputs.go_version }} - name: Update all modules diff --git a/.github/workflows/pr-gh-workflow-approve.yaml b/.github/workflows/pr-gh-workflow-approve.yaml index f493fd4003..28be4dac71 100644 --- a/.github/workflows/pr-gh-workflow-approve.yaml +++ b/.github/workflows/pr-gh-workflow-approve.yaml @@ -19,7 +19,7 @@ jobs: actions: write steps: - name: Update PR - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 353a0a1c5c..b05f2828bb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,14 +22,14 @@ jobs: id: vars run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # tag=v5.5.0 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # tag=v6.0.0 with: go-version: ${{ steps.vars.outputs.go_version }} - name: Generate release binaries run: | make release - name: Release - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # tag=v2.3.2 + uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # tag=v2.3.3 with: draft: false files: tools/setup-envtest/out/* From 585315243517a2c2c660d53f47bf721051295dde Mon Sep 17 00:00:00 2001 From: Troy Connor Date: Tue, 9 Sep 2025 17:49:56 -0400 Subject: [PATCH 06/68] =?UTF-8?q?=F0=9F=90=9BPanic=20when=20trying=20to=20?= =?UTF-8?q?build=20more=20than=20one=20instance=20of=20fake.ClientBuilder?= =?UTF-8?q?=20(#3314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * panic when trying to build more than one instance of fake.ClientBuilder Signed-off-by: Troy Connor * pr feedback Signed-off-by: Troy Connor --------- Signed-off-by: Troy Connor --- pkg/client/fake/client.go | 5 +++++ pkg/client/fake/client_test.go | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 4109b5c5eb..1d4af89abc 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -141,6 +141,7 @@ type ClientBuilder struct { interceptorFuncs *interceptor.Funcs typeConverters []managedfields.TypeConverter returnManagedFields bool + isBuilt bool // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. // The inner map maps from index name to IndexerFunc. @@ -267,6 +268,9 @@ func (f *ClientBuilder) WithReturnManagedFields() *ClientBuilder { // Build builds and returns a new fake client. func (f *ClientBuilder) Build() client.WithWatch { + if f.isBuilt { + panic("Build() must not be called multiple times when creating a ClientBuilder") + } if f.scheme == nil { f.scheme = scheme.Scheme } @@ -344,6 +348,7 @@ func (f *ClientBuilder) Build() client.WithWatch { result = interceptor.NewClient(result, *f.interceptorFuncs) } + f.isBuilt = true return result } diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 328dfeecf0..46e2b71209 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -3182,4 +3182,13 @@ var _ = Describe("Fake client builder", func() { Expect(err).NotTo(HaveOccurred()) Expect(called).To(BeTrue()) }) + + It("should panic when calling build more than once", func() { + cb := NewClientBuilder() + anotherCb := cb + cb.Build() + Expect(func() { + anotherCb.Build() + }).To(Panic()) + }) }) From 42a14a36c13b95dd6bc8b4ba69c181b16d50e3c0 Mon Sep 17 00:00:00 2001 From: dongjiang Date: Thu, 11 Sep 2025 16:15:35 +0800 Subject: [PATCH 07/68] Bump to k8s.io/* v0.34.1 Signed-off-by: dongjiang --- examples/scratch-env/go.mod | 8 ++++---- examples/scratch-env/go.sum | 16 ++++++++-------- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ tools/setup-envtest/go.mod | 2 +- tools/setup-envtest/go.sum | 4 ++-- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod index a92a25b7d8..546c7c39ee 100644 --- a/examples/scratch-env/go.mod +++ b/examples/scratch-env/go.mod @@ -54,10 +54,10 @@ 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 // indirect - k8s.io/apiextensions-apiserver v0.34.0 // indirect - k8s.io/apimachinery v0.34.0 // indirect - k8s.io/client-go v0.34.0 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/apimachinery v0.34.1 // indirect + k8s.io/client-go v0.34.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 diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum index 703b352e28..012b88f447 100644 --- a/examples/scratch-env/go.sum +++ b/examples/scratch-env/go.sum @@ -173,14 +173,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 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= -k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= -k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= -k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= -k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= -k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= 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= diff --git a/go.mod b/go.mod index 36bce9c9e5..4d998fe2fc 100644 --- a/go.mod +++ b/go.mod @@ -21,11 +21,11 @@ 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 - k8s.io/apiextensions-apiserver v0.34.0 - k8s.io/apimachinery v0.34.0 - k8s.io/apiserver v0.34.0 - k8s.io/client-go v0.34.0 + k8s.io/api v0.34.1 + k8s.io/apiextensions-apiserver v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/apiserver v0.34.1 + k8s.io/client-go v0.34.1 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/structured-merge-diff/v6 v6.3.0 @@ -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 // indirect + k8s.io/component-base v0.34.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 102a137d04..d6278d8a7d 100644 --- a/go.sum +++ b/go.sum @@ -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 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= -k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= -k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= -k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= -k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= -k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= -k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= -k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= -k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= -k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= 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= diff --git a/tools/setup-envtest/go.mod b/tools/setup-envtest/go.mod index 5cb31d8bf2..15c64f8b57 100644 --- a/tools/setup-envtest/go.mod +++ b/tools/setup-envtest/go.mod @@ -10,7 +10,7 @@ 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 + k8s.io/apimachinery v0.34.1 sigs.k8s.io/yaml v1.6.0 ) diff --git a/tools/setup-envtest/go.sum b/tools/setup-envtest/go.sum index c9dcc6499b..dfc8e7cce2 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 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= -k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.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= From 5e2c9cedf5f6110b868f8be3e3755fe7fa8d60dc Mon Sep 17 00:00:00 2001 From: dongjiang Date: Fri, 12 Sep 2025 19:48:02 +0800 Subject: [PATCH 08/68] update golangci-lint to v2.4.0 Signed-off-by: dongjiang --- .github/workflows/golangci-lint.yml | 2 +- pkg/cache/cache_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 23284914fd..0db819e6c9 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -34,6 +34,6 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # tag=v8.0.0 with: - version: v2.3.0 + version: v2.4.0 args: --output.text.print-linter-name=true --output.text.colors=true --timeout 10m working-directory: ${{matrix.working-directory}} diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 2364eec3e1..7748e2e317 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -2568,7 +2568,6 @@ func ensureNode(ctx context.Context, name string, client client.Client) error { return err } -//nolint:interfacer func isKubeService(svc metav1.Object) bool { // grumble grumble linters grumble grumble return svc.GetNamespace() == "default" && svc.GetName() == "kubernetes" From 961fc2c233d64e40b5bdf449f12bfa22cf2d7b28 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Sep 2025 04:35:45 -0400 Subject: [PATCH 09/68] :warning: Fakeclient: Fix a number of bugs when updating through apply (#3319) * Move the versioned tracked into its own file * Versioned tracker: Implement object tracker * Fakeclient Apply Update: strip status and other issues --- pkg/client/fake/client.go | 244 +----------------- pkg/client/fake/client_test.go | 103 +++++++- pkg/client/fake/versioned_tracker.go | 354 +++++++++++++++++++++++++++ 3 files changed, 466 insertions(+), 235 deletions(-) create mode 100644 pkg/client/fake/versioned_tracker.go diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 1d4af89abc..41cf233deb 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -17,13 +17,10 @@ limitations under the License. package fake import ( - "bytes" "context" "errors" "fmt" "reflect" - "runtime/debug" - "strconv" "strings" "sync" "time" @@ -65,7 +62,6 @@ import ( utilrand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" - "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/watch" clientgoapplyconfigurations "k8s.io/client-go/applyconfigurations" "k8s.io/client-go/kubernetes/scheme" @@ -79,13 +75,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/internal/objectutil" ) -type versionedTracker struct { - testing.ObjectTracker - scheme *runtime.Scheme - withStatusSubresource sets.Set[schema.GroupVersionKind] - usesFieldManagedObjectTracker bool -} - type fakeClient struct { // trackerWriteLock must be acquired before writing to // the tracker or performing reads that affect a following @@ -313,7 +302,7 @@ func (f *ClientBuilder) Build() client.WithWatch { usesFieldManagedObjectTracker = true } tracker := versionedTracker{ - ObjectTracker: f.objectTracker, + upstream: f.objectTracker, scheme: f.scheme, withStatusSubresource: withStatusSubResource, usesFieldManagedObjectTracker: usesFieldManagedObjectTracker, @@ -354,83 +343,6 @@ func (f *ClientBuilder) Build() client.WithWatch { const trackerAddResourceVersion = "999" -func (t versionedTracker) Add(obj runtime.Object) error { - var objects []runtime.Object - if meta.IsListType(obj) { - var err error - objects, err = meta.ExtractList(obj) - if err != nil { - return err - } - } else { - objects = []runtime.Object{obj} - } - for _, obj := range objects { - accessor, err := meta.Accessor(obj) - if err != nil { - return fmt.Errorf("failed to get accessor for object: %w", err) - } - if accessor.GetDeletionTimestamp() != nil && len(accessor.GetFinalizers()) == 0 { - return fmt.Errorf("refusing to create obj %s with metadata.deletionTimestamp but no finalizers", accessor.GetName()) - } - if accessor.GetResourceVersion() == "" { - // We use a "magic" value of 999 here because this field - // is parsed as uint and and 0 is already used in Update. - // As we can't go lower, go very high instead so this can - // be recognized - accessor.SetResourceVersion(trackerAddResourceVersion) - } - - obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) - if err != nil { - return err - } - - // If the fieldManager can not decode fields, it will just silently clear them. This is pretty - // much guaranteed not to be what someone that initializes a fake client with objects that - // have them set wants, so validate them here. - // Ref https://github.com/kubernetes/kubernetes/blob/a956ef4862993b825bcd524a19260192ff1da72d/staging/src/k8s.io/apimachinery/pkg/util/managedfields/internal/fieldmanager.go#L105 - if t.usesFieldManagedObjectTracker { - if err := managedfields.ValidateManagedFields(accessor.GetManagedFields()); err != nil { - return fmt.Errorf("invalid managedFields on %T: %w", obj, err) - } - } - if err := t.ObjectTracker.Add(obj); err != nil { - return err - } - } - - return nil -} - -func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.CreateOptions) error { - accessor, err := meta.Accessor(obj) - if err != nil { - return fmt.Errorf("failed to get accessor for object: %w", err) - } - if accessor.GetName() == "" { - gvk, _ := apiutil.GVKForObject(obj, t.scheme) - return apierrors.NewInvalid( - gvk.GroupKind(), - accessor.GetName(), - field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) - } - if accessor.GetResourceVersion() != "" { - return apierrors.NewBadRequest("resourceVersion can not be set for Create requests") - } - accessor.SetResourceVersion("1") - obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) - if err != nil { - return err - } - if err := t.ObjectTracker.Create(gvr, obj, ns, opts...); err != nil { - accessor.SetResourceVersion("") - return err - } - - return nil -} - // convertFromUnstructuredIfNecessary will convert runtime.Unstructured for a GVK that is recognized // by the schema into the whatever the schema produces with New() for said GVK. // This is required because the tracker unconditionally saves on manipulations, but its List() implementation @@ -465,151 +377,6 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru return typed, nil } -func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.UpdateOptions) error { - updateOpts, err := getSingleOrZeroOptions(opts) - if err != nil { - return err - } - - return t.update(gvr, obj, ns, false, false, updateOpts) -} - -func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, opts metav1.UpdateOptions) error { - gvk, err := apiutil.GVKForObject(obj, t.scheme) - if err != nil { - return err - } - obj, err = t.updateObject(gvr, obj, ns, isStatus, deleting, opts.DryRun) - if err != nil { - return err - } - if obj == nil { - return nil - } - - if u, unstructured := obj.(*unstructured.Unstructured); unstructured { - u.SetGroupVersionKind(gvk) - } - - return t.ObjectTracker.Update(gvr, obj, ns, opts) -} - -func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.PatchOptions) error { - patchOptions, err := getSingleOrZeroOptions(opts) - if err != nil { - return err - } - - // We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change - // that reaction, we use the callstack to figure out if this originated from the status client. - isStatus := bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch")) - - obj, err = t.updateObject(gvr, obj, ns, isStatus, false, patchOptions.DryRun) - if err != nil { - return err - } - if obj == nil { - return nil - } - - return t.ObjectTracker.Patch(gvr, obj, ns, patchOptions) -} - -func (t versionedTracker) updateObject(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, dryRun []string) (runtime.Object, error) { - accessor, err := meta.Accessor(obj) - if err != nil { - return nil, fmt.Errorf("failed to get accessor for object: %w", err) - } - - if accessor.GetName() == "" { - gvk, _ := apiutil.GVKForObject(obj, t.scheme) - return nil, apierrors.NewInvalid( - gvk.GroupKind(), - accessor.GetName(), - field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) - } - - gvk, err := apiutil.GVKForObject(obj, t.scheme) - if err != nil { - return nil, err - } - - oldObject, err := t.ObjectTracker.Get(gvr, ns, accessor.GetName()) - if err != nil { - // If the resource is not found and the resource allows create on update, issue a - // create instead. - if apierrors.IsNotFound(err) && allowsCreateOnUpdate(gvk) { - return nil, t.Create(gvr, obj, ns) - } - return nil, err - } - - if t.withStatusSubresource.Has(gvk) { - if isStatus { // copy everything but status and metadata.ResourceVersion from original object - if err := copyStatusFrom(obj, oldObject); err != nil { - return nil, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err) - } - passedRV := accessor.GetResourceVersion() - if err := copyFrom(oldObject, obj); err != nil { - return nil, fmt.Errorf("failed to restore non-status fields: %w", err) - } - accessor.SetResourceVersion(passedRV) - } else { // copy status from original object - if err := copyStatusFrom(oldObject, obj); err != nil { - return nil, fmt.Errorf("failed to copy the status for object with status subresource: %w", err) - } - } - } else if isStatus { - return nil, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName()) - } - - oldAccessor, err := meta.Accessor(oldObject) - if err != nil { - return nil, err - } - - // If the new object does not have the resource version set and it allows unconditional update, - // default it to the resource version of the existing resource - if accessor.GetResourceVersion() == "" { - switch { - case allowsUnconditionalUpdate(gvk): - accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) - // This is needed because if the patch explicitly sets the RV to null, the client-go reaction we use - // to apply it and whose output we process here will have it unset. It is not clear why the Kubernetes - // apiserver accepts such a patch, but it does so we just copy that behavior. - // Kubernetes apiserver behavior can be checked like this: - // `kubectl patch configmap foo --patch '{"metadata":{"annotations":{"foo":"bar"},"resourceVersion":null}}' -v=9` - case bytes. - Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")): - // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change - // that reaction, we use the callstack to figure out if this originated from the "fakeClient.Patch" func. - accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) - } - } - - if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() { - return nil, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified")) - } - if oldAccessor.GetResourceVersion() == "" { - oldAccessor.SetResourceVersion("0") - } - intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64) - if err != nil { - return nil, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err) - } - intResourceVersion++ - accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10)) - - if !deleting && !deletionTimestampEqual(accessor, oldAccessor) { - return nil, fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName()) - } - - if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 { - return nil, t.ObjectTracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun}) - } - return convertFromUnstructuredIfNecessary(t.scheme, obj) -} - func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { if err := c.addToSchemeIfUnknownAndUnstructuredOrPartial(obj); err != nil { return err @@ -1233,6 +1000,15 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client reaction := testing.ObjectReaction(c.tracker) handled, o, err := reaction(action) if err != nil { + // The reaction calls tracker.Get after tracker.Apply to return the object, + // but we may have deleted it in tracker.Apply if there was no finalizer + // left. + if apierrors.IsNotFound(err) && + patch.Type() == types.ApplyPatchType && + oldAccessor.GetDeletionTimestamp() != nil && + len(obj.GetFinalizers()) == 0 { + return nil + } return err } if !handled { diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 46e2b71209..23f52b9fb8 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -660,6 +660,19 @@ var _ = Describe("Fake client", func() { Expect(obj.ObjectMeta.ResourceVersion).To(Equal(trackerAddResourceVersion)) }) + It("should reject apply with non-matching ResourceVersion", func(ctx SpecContext) { + cl := NewClientBuilder().WithRuntimeObjects(cm).Build() + applyCM := corev1applyconfigurations.ConfigMap(cm.Name, cm.Namespace).WithResourceVersion("0") + err := cl.Apply(ctx, applyCM, client.FieldOwner("test")) + Expect(apierrors.IsConflict(err)).To(BeTrue()) + + obj := &corev1.ConfigMap{} + err = cl.Get(ctx, client.ObjectKeyFromObject(cm), obj) + Expect(err).ToNot(HaveOccurred()) + Expect(obj).To(Equal(cm)) + Expect(obj.ObjectMeta.ResourceVersion).To(Equal(trackerAddResourceVersion)) + }) + It("should reject Delete with a mismatched ResourceVersion", func(ctx SpecContext) { bogusRV := "bogus" By("Deleting with a mismatched ResourceVersion Precondition") @@ -714,6 +727,35 @@ var _ = Describe("Fake client", func() { Expect(list.Items).To(ConsistOf(*dep2)) }) + It("should handle finalizers in Apply ", func(ctx SpecContext) { + cl := client.WithFieldOwner(cl, "test") + + By("Creating the object with a finalizer") + cm := corev1applyconfigurations.ConfigMap("test-cm", "delete-with-finalizers"). + WithFinalizers("finalizers.sigs.k8s.io/test") + Expect(cl.Apply(ctx, cm)).To(Succeed()) + + By("Deleting the object") + obj := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{ + Name: *cm.Name, + Namespace: *cm.Namespace, + }} + Expect(cl.Delete(ctx, obj)).NotTo(HaveOccurred()) + + By("Getting the object") + Expect(cl.Get(ctx, client.ObjectKeyFromObject(obj), obj)).NotTo(HaveOccurred()) + Expect(obj.DeletionTimestamp).NotTo(BeNil()) + + By("Removing the finalizer through SSA") + cm.ResourceVersion = nil + cm.Finalizers = nil + Expect(cl.Apply(ctx, cm)).NotTo(HaveOccurred()) + + By("Getting the object") + err := cl.Get(ctx, client.ObjectKeyFromObject(obj), &corev1.ConfigMap{}) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + It("should handle finalizers on Update", func(ctx SpecContext) { namespacedName := types.NamespacedName{ Name: "test-cm", @@ -1733,6 +1775,40 @@ var _ = Describe("Fake client", func() { Expect(cmp.Diff(objOriginal, actual)).To(BeEmpty()) }) + It("should not change the status of objects with status subresource when creating through apply ", func(ctx SpecContext) { + obj := corev1applyconfigurations. + Pod("node", ""). + WithStatus( + corev1applyconfigurations.PodStatus().WithPhase("Running"), + ) + + cl := NewClientBuilder().WithStatusSubresource(&corev1.Pod{}).Build() + Expect(cl.Apply(ctx, obj, client.FieldOwner("test"))).To(Succeed()) + + p := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: *obj.Name}} + Expect(cl.Get(ctx, client.ObjectKeyFromObject(p), p)).To(Succeed()) + + Expect(p.Status).To(BeComparableTo(corev1.PodStatus{})) + }) + + It("should not change the status of objects with status subresource when updating through apply ", func(ctx SpecContext) { + + cl := NewClientBuilder().WithStatusSubresource(&corev1.Pod{}).Build() + pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod"}} + Expect(cl.Create(ctx, pod)).NotTo(HaveOccurred()) + + obj := corev1applyconfigurations. + Pod(pod.Name, ""). + WithStatus( + corev1applyconfigurations.PodStatus().WithPhase("Running"), + ) + Expect(cl.Apply(ctx, obj, client.FieldOwner("test"))).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(pod), pod)).To(Succeed()) + + Expect(pod.Status).To(BeComparableTo(corev1.PodStatus{})) + }) + It("should Unmarshal the schemaless object with int64 to preserve ints", func(ctx SpecContext) { schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} schemeBuilder.Register(&WithSchemalessSpec{}) @@ -2781,6 +2857,17 @@ var _ = Describe("Fake client", func() { Expect(cm.Data).To(BeComparableTo(map[string]string{"other": "data"})) }) + It("returns a conflict when trying to Create an object with UID set through Apply", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + obj := corev1applyconfigurations. + ConfigMap("foo", "default"). + WithUID("123") + + err := cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsConflict(err)).To(BeTrue()) + }) + It("errors when trying to server-side apply an object without configuring a FieldManager", func(ctx SpecContext) { cl := NewClientBuilder().Build() obj := corev1applyconfigurations. @@ -2827,7 +2914,7 @@ 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) { + It("sets the fieldManager in create, patch and update", func(ctx SpecContext) { owner := "test-owner" cl := client.WithFieldOwner( NewClientBuilder().WithReturnManagedFields().Build(), @@ -2861,6 +2948,20 @@ var _ = Describe("Fake client", func() { } }) + It("sets the fieldManager when creating through update", func(ctx SpecContext) { + owner := "test-owner" + cl := client.WithFieldOwner( + NewClientBuilder().WithReturnManagedFields().Build(), + owner, + ) + + obj := &corev1.Event{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + Expect(cl.Update(ctx, obj, client.FieldOwner(owner))).NotTo(HaveOccurred()) + 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. diff --git a/pkg/client/fake/versioned_tracker.go b/pkg/client/fake/versioned_tracker.go new file mode 100644 index 0000000000..c1caa1ca02 --- /dev/null +++ b/pkg/client/fake/versioned_tracker.go @@ -0,0 +1,354 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fake + +import ( + "bytes" + "errors" + "fmt" + "runtime/debug" + "strconv" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/managedfields" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/testing" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" +) + +var _ testing.ObjectTracker = (*versionedTracker)(nil) + +type versionedTracker struct { + upstream testing.ObjectTracker + scheme *runtime.Scheme + withStatusSubresource sets.Set[schema.GroupVersionKind] + usesFieldManagedObjectTracker bool +} + +func (t versionedTracker) Add(obj runtime.Object) error { + var objects []runtime.Object + if meta.IsListType(obj) { + var err error + objects, err = meta.ExtractList(obj) + if err != nil { + return err + } + } else { + objects = []runtime.Object{obj} + } + for _, obj := range objects { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("failed to get accessor for object: %w", err) + } + if accessor.GetDeletionTimestamp() != nil && len(accessor.GetFinalizers()) == 0 { + return fmt.Errorf("refusing to create obj %s with metadata.deletionTimestamp but no finalizers", accessor.GetName()) + } + if accessor.GetResourceVersion() == "" { + // We use a "magic" value of 999 here because this field + // is parsed as uint and and 0 is already used in Update. + // As we can't go lower, go very high instead so this can + // be recognized + accessor.SetResourceVersion(trackerAddResourceVersion) + } + + obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) + if err != nil { + return err + } + + // If the fieldManager can not decode fields, it will just silently clear them. This is pretty + // much guaranteed not to be what someone that initializes a fake client with objects that + // have them set wants, so validate them here. + // Ref https://github.com/kubernetes/kubernetes/blob/a956ef4862993b825bcd524a19260192ff1da72d/staging/src/k8s.io/apimachinery/pkg/util/managedfields/internal/fieldmanager.go#L105 + if t.usesFieldManagedObjectTracker { + if err := managedfields.ValidateManagedFields(accessor.GetManagedFields()); err != nil { + return fmt.Errorf("invalid managedFields on %T: %w", obj, err) + } + } + if err := t.upstream.Add(obj); err != nil { + return err + } + } + + return nil +} + +func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.CreateOptions) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("failed to get accessor for object: %w", err) + } + if accessor.GetName() == "" { + gvk, _ := apiutil.GVKForObject(obj, t.scheme) + return apierrors.NewInvalid( + gvk.GroupKind(), + accessor.GetName(), + field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) + } + if accessor.GetResourceVersion() != "" { + return apierrors.NewBadRequest("resourceVersion can not be set for Create requests") + } + accessor.SetResourceVersion("1") + obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) + if err != nil { + return err + } + if err := t.upstream.Create(gvr, obj, ns, opts...); err != nil { + accessor.SetResourceVersion("") + return err + } + + return nil +} + +func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.UpdateOptions) error { + updateOpts, err := getSingleOrZeroOptions(opts) + if err != nil { + return err + } + + return t.update(gvr, obj, ns, false, false, updateOpts) +} + +func (t versionedTracker) update(gvr schema.GroupVersionResource, obj runtime.Object, ns string, isStatus, deleting bool, opts metav1.UpdateOptions) error { + gvk, err := apiutil.GVKForObject(obj, t.scheme) + if err != nil { + return err + } + obj, needsCreate, err := t.updateObject(gvr, gvk, obj, ns, isStatus, deleting, allowsCreateOnUpdate(gvk), opts.DryRun) + if err != nil { + return err + } + + if needsCreate { + opts := metav1.CreateOptions{DryRun: opts.DryRun, FieldManager: opts.FieldManager} + return t.Create(gvr, obj, ns, opts) + } + + if obj == nil { // Object was deleted in updateObject + return nil + } + + if u, unstructured := obj.(*unstructured.Unstructured); unstructured { + u.SetGroupVersionKind(gvk) + } + + return t.upstream.Update(gvr, obj, ns, opts) +} + +func (t versionedTracker) Patch(gvr schema.GroupVersionResource, obj runtime.Object, ns string, opts ...metav1.PatchOptions) error { + patchOptions, err := getSingleOrZeroOptions(opts) + if err != nil { + return err + } + + gvk, err := apiutil.GVKForObject(obj, t.scheme) + if err != nil { + return err + } + + // We apply patches using a client-go reaction that ends up calling the trackers Patch. As we can't change + // that reaction, we use the callstack to figure out if this originated from the status client. + isStatus := bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch")) + + obj, needsCreate, err := t.updateObject(gvr, gvk, obj, ns, isStatus, false, allowsCreateOnUpdate(gvk), patchOptions.DryRun) + if err != nil { + return err + } + if needsCreate { + opts := metav1.CreateOptions{DryRun: patchOptions.DryRun, FieldManager: patchOptions.FieldManager} + return t.Create(gvr, obj, ns, opts) + } + + if obj == nil { // Object was deleted in updateObject + return nil + } + + return t.upstream.Patch(gvr, obj, ns, patchOptions) +} + +// updateObject performs a number of validations and changes related to +// object updates, such as checking and updating the resourceVersion. +func (t versionedTracker) updateObject( + gvr schema.GroupVersionResource, + gvk schema.GroupVersionKind, + obj runtime.Object, + ns string, + isStatus bool, + deleting bool, + allowCreateOnUpdate bool, + dryRun []string, +) (result runtime.Object, needsCreate bool, _ error) { + accessor, err := meta.Accessor(obj) + if err != nil { + return nil, false, fmt.Errorf("failed to get accessor for object: %w", err) + } + + if accessor.GetName() == "" { + return nil, false, apierrors.NewInvalid( + gvk.GroupKind(), + accessor.GetName(), + field.ErrorList{field.Required(field.NewPath("metadata.name"), "name is required")}) + } + + oldObject, err := t.Get(gvr, ns, accessor.GetName()) + if err != nil { + // If the resource is not found and the resource allows create on update, issue a + // create instead. + if apierrors.IsNotFound(err) && allowCreateOnUpdate { + // Pass this info to the caller rather than create, because in the SSA case it + // must be created by calling Apply in the upstream tracker, not Create. + // This is because SSA considers Apply and Non-Apply operations to be different + // even when they use the same fieldManager. This behavior is also observable + // with a real Kubernetes apiserver. + // + // Ref https://kubernetes.slack.com/archives/C0EG7JC6T/p1757868204458989?thread_ts=1757808656.002569&cid=C0EG7JC6T + return obj, true, nil + } + return obj, false, err + } + + if t.withStatusSubresource.Has(gvk) { + if isStatus { // copy everything but status and metadata.ResourceVersion from original object + if err := copyStatusFrom(obj, oldObject); err != nil { + return nil, false, fmt.Errorf("failed to copy non-status field for object with status subresouce: %w", err) + } + passedRV := accessor.GetResourceVersion() + if err := copyFrom(oldObject, obj); err != nil { + return nil, false, fmt.Errorf("failed to restore non-status fields: %w", err) + } + accessor.SetResourceVersion(passedRV) + } else { // copy status from original object + if err := copyStatusFrom(oldObject, obj); err != nil { + return nil, false, fmt.Errorf("failed to copy the status for object with status subresource: %w", err) + } + } + } else if isStatus { + return nil, false, apierrors.NewNotFound(gvr.GroupResource(), accessor.GetName()) + } + + oldAccessor, err := meta.Accessor(oldObject) + if err != nil { + return nil, false, err + } + + // If the new object does not have the resource version set and it allows unconditional update, + // default it to the resource version of the existing resource + if accessor.GetResourceVersion() == "" { + switch { + case allowsUnconditionalUpdate(gvk): + accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) + // This is needed because if the patch explicitly sets the RV to null, the client-go reaction we use + // to apply it and whose output we process here will have it unset. It is not clear why the Kubernetes + // apiserver accepts such a patch, but it does so we just copy that behavior. + // Kubernetes apiserver behavior can be checked like this: + // `kubectl patch configmap foo --patch '{"metadata":{"annotations":{"foo":"bar"},"resourceVersion":null}}' -v=9` + case bytes. + Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")): + // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change + // that reaction, we use the callstack to figure out if this originated from the "fakeClient.Patch" func. + accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) + } + } + + if accessor.GetResourceVersion() != oldAccessor.GetResourceVersion() { + return nil, false, apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), errors.New("object was modified")) + } + if oldAccessor.GetResourceVersion() == "" { + oldAccessor.SetResourceVersion("0") + } + intResourceVersion, err := strconv.ParseUint(oldAccessor.GetResourceVersion(), 10, 64) + if err != nil { + return nil, false, fmt.Errorf("can not convert resourceVersion %q to int: %w", oldAccessor.GetResourceVersion(), err) + } + intResourceVersion++ + accessor.SetResourceVersion(strconv.FormatUint(intResourceVersion, 10)) + + if !deleting && !deletionTimestampEqual(accessor, oldAccessor) { + return nil, false, fmt.Errorf("error: Unable to edit %s: metadata.deletionTimestamp field is immutable", accessor.GetName()) + } + + if !accessor.GetDeletionTimestamp().IsZero() && len(accessor.GetFinalizers()) == 0 { + return nil, false, t.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun}) + } + + obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) + return obj, false, err +} + +func (t versionedTracker) Apply(gvr schema.GroupVersionResource, applyConfiguration runtime.Object, ns string, opts ...metav1.PatchOptions) error { + patchOptions, err := getSingleOrZeroOptions(opts) + if err != nil { + return err + } + gvk, err := apiutil.GVKForObject(applyConfiguration, t.scheme) + if err != nil { + return err + } + applyConfiguration, needsCreate, err := t.updateObject(gvr, gvk, applyConfiguration, ns, false, false, true, patchOptions.DryRun) + if err != nil { + return err + } + + if needsCreate { + // https://github.com/kubernetes/kubernetes/blob/81affffa1b8d8079836f4cac713ea8d1b2bbf10f/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go#L606 + accessor, err := meta.Accessor(applyConfiguration) + if err != nil { + return fmt.Errorf("failed to get accessor for object: %w", err) + } + if accessor.GetUID() != "" { + return apierrors.NewConflict(gvr.GroupResource(), accessor.GetName(), fmt.Errorf("uid mismatch: the provided object specified uid %s, and no existing object was found", accessor.GetUID())) + } + + if t.withStatusSubresource.Has(gvk) { + // Clear out status for create, for update this is handled in updateObject + if err := copyStatusFrom(&unstructured.Unstructured{}, applyConfiguration); err != nil { + return err + } + } + } + + if applyConfiguration == nil { // Object was deleted in updateObject + return nil + } + + return t.upstream.Apply(gvr, applyConfiguration, ns, opts...) +} + +func (t versionedTracker) Delete(gvr schema.GroupVersionResource, ns, name string, opts ...metav1.DeleteOptions) error { + return t.upstream.Delete(gvr, ns, name, opts...) +} + +func (t versionedTracker) Get(gvr schema.GroupVersionResource, ns, name string, opts ...metav1.GetOptions) (runtime.Object, error) { + return t.upstream.Get(gvr, ns, name, opts...) +} + +func (t versionedTracker) List(gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, ns string, opts ...metav1.ListOptions) (runtime.Object, error) { + return t.upstream.List(gvr, gvk, ns, opts...) +} + +func (t versionedTracker) Watch(gvr schema.GroupVersionResource, ns string, opts ...metav1.ListOptions) (watch.Interface, error) { + return t.upstream.Watch(gvr, ns, opts...) +} From 5c9496f3dd4942f54111b0854af97f6495198ba7 Mon Sep 17 00:00:00 2001 From: Dmitry Volodin Date: Wed, 24 Sep 2025 13:10:13 +0300 Subject: [PATCH 10/68] :sparkles: Able to set WithContextFunc in WebhookBuilder --- pkg/builder/webhook.go | 10 +++++++ pkg/builder/webhook_test.go | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go index 6263f030a0..6f4726d274 100644 --- a/pkg/builder/webhook.go +++ b/pkg/builder/webhook.go @@ -17,6 +17,7 @@ limitations under the License. package builder import ( + "context" "errors" "net/http" "net/url" @@ -49,6 +50,7 @@ type WebhookBuilder struct { config *rest.Config recoverPanic *bool logConstructor func(base logr.Logger, req *admission.Request) logr.Logger + contextFunc func(context.Context, *http.Request) context.Context err error } @@ -90,6 +92,12 @@ func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Lo return blder } +// WithContextFunc overrides the webhook's WithContextFunc. +func (blder *WebhookBuilder) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder { + blder.contextFunc = contextFunc + return blder +} + // RecoverPanic indicates whether panics caused by the webhook should be recovered. // Defaults to true. func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder { @@ -205,6 +213,7 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() error { mwh := blder.getDefaultingWebhook() if mwh != nil { mwh.LogConstructor = blder.logConstructor + mwh.WithContextFunc = blder.contextFunc path := generateMutatePath(blder.gvk) if blder.customDefaulterCustomPath != "" { generatedCustomPath, err := generateCustomPath(blder.customDefaulterCustomPath) @@ -243,6 +252,7 @@ func (blder *WebhookBuilder) registerValidatingWebhook() error { vwh := blder.getValidatingWebhook() if vwh != nil { vwh.LogConstructor = blder.logConstructor + vwh.WithContextFunc = blder.contextFunc path := generateValidatePath(blder.gvk) if blder.customValidatorCustomPath != "" { generatedCustomPath, err := generateCustomPath(blder.customValidatorCustomPath) diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go index eb70af2e0a..72538ef7bf 100644 --- a/pkg/builder/webhook_test.go +++ b/pkg/builder/webhook_test.go @@ -49,8 +49,14 @@ const ( svcBaseAddr = "http://svc-name.svc-ns.svc" customPath = "/custom-path" + + userAgentHeader = "User-Agent" + userAgentCtxKey agentCtxKey = "UserAgent" + userAgentValue = "test" ) +type agentCtxKey string + var _ = Describe("webhook", func() { Describe("New", func() { Context("v1 AdmissionReview", func() { @@ -315,6 +321,9 @@ func runTests(admissionReviewVersion string) { WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { return admission.DefaultLogConstructor(testingLogger, req) }). + WithContextFunc(func(ctx context.Context, request *http.Request) context.Context { + return context.WithValue(ctx, userAgentCtxKey, request.Header.Get(userAgentHeader)) + }). Complete() ExpectWithOffset(1, err).NotTo(HaveOccurred()) svr := m.GetWebhookServer() @@ -344,6 +353,30 @@ func runTests(admissionReviewVersion string) { } } }`) + readerWithCxt := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + "request":{ + "uid":"07e52e8d-4513-11e9-a716-42010a800270", + "kind":{ + "group":"foo.test.org", + "version":"v1", + "kind":"TestValidator" + }, + "resource":{ + "group":"foo.test.org", + "version":"v1", + "resource":"testvalidator" + }, + "namespace":"default", + "name":"foo", + "operation":"UPDATE", + "object":{ + "replica":1 + }, + "oldObject":{ + "replica":1 + } + } +}`) ctx, cancel := context.WithCancel(specCtx) cancel() @@ -373,6 +406,20 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook with context header validation") + path = generateValidatePath(testValidatorGVK) + _, err = readerWithCxt.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, readerWithCxt) + req.Header.Add("Content-Type", "application/json") + req.Header.Add(userAgentHeader, userAgentValue) + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) }) It("should scaffold a custom validating webhook with a custom path", func(specCtx SpecContext) { @@ -1009,6 +1056,7 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err if d.Replica < 2 { d.Replica = 2 } + return nil } @@ -1035,6 +1083,7 @@ func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Obje if v.Replica < 0 { return nil, errors.New("number of replica should be greater than or equal to 0") } + return nil, nil } @@ -1056,6 +1105,12 @@ func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj r if v.Replica < old.Replica { return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", v.Replica, old.Replica) } + + userAgent, ok := ctx.Value(userAgentCtxKey).(string) + if ok && userAgent != userAgentValue { + return nil, fmt.Errorf("expected %s value is %q in TestCustomValidator got %q", userAgentCtxKey, userAgentValue, userAgent) + } + return nil, nil } From a56f421d83c5317b13f8d323b1fe1c0e1eb6bbd4 Mon Sep 17 00:00:00 2001 From: Manoj Sudheendra Date: Fri, 26 Sep 2025 16:03:37 -0400 Subject: [PATCH 11/68] Copy all parent context values to leader elector's context --- pkg/manager/internal.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/manager/internal.go b/pkg/manager/internal.go index a9f91cbdd5..a2c3e5324d 100644 --- a/pkg/manager/internal.go +++ b/pkg/manager/internal.go @@ -446,13 +446,16 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) { // Start the leader election and all required runnables. { - ctx, cancel := context.WithCancel(context.Background()) + // Create a context that inherits all keys from the parent context + // but can be cancelled independently for leader election management + baseCtx := context.WithoutCancel(ctx) + leaderCtx, cancel := context.WithCancel(baseCtx) cm.leaderElectionCancel = cancel if leaderElector != nil { // Start the leader elector process go func() { - leaderElector.Run(ctx) - <-ctx.Done() + leaderElector.Run(leaderCtx) + <-leaderCtx.Done() close(cm.leaderElectionStopped) }() } else { From f80753f2fdc227b30c1258560fcc6f0d83869de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Bavelier?= Date: Mon, 29 Sep 2025 11:12:20 +0200 Subject: [PATCH 12/68] Modernize finalizer utils --- pkg/controller/controllerutil/controllerutil.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/pkg/controller/controllerutil/controllerutil.go b/pkg/controller/controllerutil/controllerutil.go index 0088f88e5d..0f12b934ee 100644 --- a/pkg/controller/controllerutil/controllerutil.go +++ b/pkg/controller/controllerutil/controllerutil.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "reflect" + "slices" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -501,10 +502,8 @@ type MutateFn func() error // It returns an indication of whether it updated the object's list of finalizers. func AddFinalizer(o client.Object, finalizer string) (finalizersUpdated bool) { f := o.GetFinalizers() - for _, e := range f { - if e == finalizer { - return false - } + if slices.Contains(f, finalizer) { + return false } o.SetFinalizers(append(f, finalizer)) return true @@ -517,7 +516,7 @@ func RemoveFinalizer(o client.Object, finalizer string) (finalizersUpdated bool) length := len(f) index := 0 - for i := 0; i < length; i++ { + for i := range length { if f[i] == finalizer { continue } @@ -531,10 +530,5 @@ func RemoveFinalizer(o client.Object, finalizer string) (finalizersUpdated bool) // ContainsFinalizer checks an Object that the provided finalizer is present. func ContainsFinalizer(o client.Object, finalizer string) bool { f := o.GetFinalizers() - for _, e := range f { - if e == finalizer { - return true - } - } - return false + return slices.Contains(f, finalizer) } From 655fb2ceb9f26640b0281fd5c3b66fcd58967ea6 Mon Sep 17 00:00:00 2001 From: dongjiang Date: Fri, 3 Oct 2025 14:32:56 +0800 Subject: [PATCH 13/68] update golangci-lint to v2.5.0 (#3323) update golangci-lint to v2.5.0 Update examples/tokenreview/tokenreview.go Update pkg/reconcile/reconcile.go Co-authored-by: Christian Schlotter --- .github/workflows/golangci-lint.yml | 2 +- .golangci.yml | 2 ++ examples/crd/pkg/groupversion_info.go | 1 + examples/tokenreview/tokenreview.go | 2 +- pkg/log/zap/flags.go | 2 -- pkg/reconcile/reconcile.go | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 0db819e6c9..2bbbb33aea 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -34,6 +34,6 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # tag=v8.0.0 with: - version: v2.4.0 + version: v2.5.0 args: --output.text.print-linter-name=true --output.text.colors=true --timeout 10m working-directory: ${{matrix.working-directory}} diff --git a/.golangci.yml b/.golangci.yml index 1741432a01..85701c88a8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,10 +22,12 @@ linters: - goconst - gocritic - gocyclo + - godoclint - goprintffuncname - govet - importas - ineffassign + - iotamixing - makezero - misspell - nakedret diff --git a/examples/crd/pkg/groupversion_info.go b/examples/crd/pkg/groupversion_info.go index 31dfbbc779..693d255b05 100644 --- a/examples/crd/pkg/groupversion_info.go +++ b/examples/crd/pkg/groupversion_info.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package pkg contains API Schema definitions for the chaosapps v1 API group // +kubebuilder:object:generate=true // +groupName=chaosapps.metamagical.io package pkg diff --git a/examples/tokenreview/tokenreview.go b/examples/tokenreview/tokenreview.go index cc64545e16..16e4151077 100644 --- a/examples/tokenreview/tokenreview.go +++ b/examples/tokenreview/tokenreview.go @@ -28,7 +28,7 @@ import ( type authenticator struct { } -// authenticator admits a request by the token. +// Handle admits a request by the token. func (a *authenticator) Handle(ctx context.Context, req authentication.Request) authentication.Response { if req.Spec.Token == "invalid" { return authentication.Unauthenticated("invalid is an invalid token", v1.UserInfo{}) diff --git a/pkg/log/zap/flags.go b/pkg/log/zap/flags.go index 2c88ad42ab..4ebac57dcb 100644 --- a/pkg/log/zap/flags.go +++ b/pkg/log/zap/flags.go @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package zap contains helpers for setting up a new logr.Logger instance -// using the Zap logging framework. package zap import ( diff --git a/pkg/reconcile/reconcile.go b/pkg/reconcile/reconcile.go index c98b1864ef..eed06a08b3 100644 --- a/pkg/reconcile/reconcile.go +++ b/pkg/reconcile/reconcile.go @@ -174,7 +174,7 @@ type terminalError struct { err error } -// This function will return nil if te.err is nil. +// Unwrap returns nil if te.err is nil. func (te *terminalError) Unwrap() error { return te.err } From c5d90e3265e0f0ba7d3c75762729b854e2c94b83 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sat, 4 Oct 2025 20:25:57 -0400 Subject: [PATCH 14/68] :seedling: Prioriyqueue tests: Add and use newQueueWithTimeForwarder We have multiple tests that advance the queue time through a custom now func and ticker, move that into a common helper. No functional changes in either the workqueue or tests. --- .../priorityqueue/priorityqueue_test.go | 110 ++++++++---------- 1 file changed, 47 insertions(+), 63 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 13cf59b7e8..10e8f7122d 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -93,11 +93,10 @@ var _ = Describe("Controllerworkqueue", func() { q.AddWithOpts(AddOpts{}, "foo") q.AddWithOpts(AddOpts{}, "foo") - Consistently(q.Len).Should(Equal(1)) + Expect(q.Len()).To(Equal(1)) - cwq := q.(*priorityqueue[string]) - cwq.lockedLock.Lock() - Expect(cwq.locked.Len()).To(Equal(0)) + q.lockedLock.Lock() + Expect(q.locked.Len()).To(Equal(0)) Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 1})) Expect(metrics.adds["test"]).To(Equal(1)) @@ -156,22 +155,13 @@ var _ = Describe("Controllerworkqueue", func() { }) It("returns an item only after after has passed", func() { - q, metrics := newQueue() + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() defer q.ShutDown() - now := time.Now().Round(time.Second) - nowLock := sync.Mutex{} - tick := make(chan time.Time) - - cwq := q.(*priorityqueue[string]) - cwq.now = func() time.Time { - nowLock.Lock() - defer nowLock.Unlock() - return now - } - cwq.tick = func(d time.Duration) <-chan time.Time { + originalTick := q.tick + q.tick = func(d time.Duration) <-chan time.Time { Expect(d).To(Equal(time.Second)) - return tick + return originalTick(d) } retrievedItem := make(chan struct{}) @@ -186,10 +176,7 @@ var _ = Describe("Controllerworkqueue", func() { Consistently(retrievedItem).ShouldNot(BeClosed()) - nowLock.Lock() - now = now.Add(time.Second) - nowLock.Unlock() - tick <- now + forwardQueueTimeBy(time.Second) Eventually(retrievedItem).Should(BeClosed()) Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) @@ -223,20 +210,11 @@ var _ = Describe("Controllerworkqueue", func() { }) It("returns multiple items with after in correct order", func() { - q, metrics := newQueue() + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() defer q.ShutDown() - now := time.Now().Round(time.Second) - nowLock := sync.Mutex{} - tick := make(chan time.Time) - - cwq := q.(*priorityqueue[string]) - cwq.now = func() time.Time { - nowLock.Lock() - defer nowLock.Unlock() - return now - } - cwq.tick = func(d time.Duration) <-chan time.Time { + originalTick := q.tick + q.tick = func(d time.Duration) <-chan time.Time { // What a bunch of bs. Deferring in here causes // ginkgo to deadlock, presumably because it // never returns after the defer. Not deferring @@ -254,7 +232,7 @@ var _ = Describe("Controllerworkqueue", func() { Expect(d).To(Or(Equal(200*time.Millisecond), Equal(time.Second))) }() <-done - return tick + return originalTick(d) } retrievedItem := make(chan struct{}) @@ -276,10 +254,7 @@ var _ = Describe("Controllerworkqueue", func() { Consistently(retrievedItem).ShouldNot(BeClosed()) - nowLock.Lock() - now = now.Add(time.Second) - nowLock.Unlock() - tick <- now + forwardQueueTimeBy(time.Second) Eventually(retrievedItem).Should(BeClosed()) Eventually(retrievedSecondItem).Should(BeClosed()) @@ -462,21 +437,12 @@ var _ = Describe("Controllerworkqueue", func() { }) It("When adding items with rateLimit, previous items' rateLimit should not affect subsequent items", func() { - q, metrics := newQueue() + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() defer q.ShutDown() - now := time.Now().Round(time.Second) - nowLock := sync.Mutex{} - tick := make(chan time.Time) - - cwq := q.(*priorityqueue[string]) - cwq.rateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[string](5*time.Millisecond, 1000*time.Second) - cwq.now = func() time.Time { - nowLock.Lock() - defer nowLock.Unlock() - return now - } - cwq.tick = func(d time.Duration) <-chan time.Time { + q.rateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[string](5*time.Millisecond, 1000*time.Second) + originalTick := q.tick + q.tick = func(d time.Duration) <-chan time.Time { done := make(chan struct{}) go func() { defer GinkgoRecover() @@ -485,7 +451,7 @@ var _ = Describe("Controllerworkqueue", func() { Expect(d).To(Or(Equal(5*time.Millisecond), Equal(635*time.Millisecond))) }() <-done - return tick + return originalTick(d) } retrievedItem := make(chan struct{}) @@ -504,22 +470,16 @@ var _ = Describe("Controllerworkqueue", func() { // after 7 calls, the next When("bar") call will return 640ms. for range 7 { - cwq.rateLimiter.When("bar") + q.rateLimiter.When("bar") } q.AddWithOpts(AddOpts{RateLimited: true}, "foo", "bar") Consistently(retrievedItem).ShouldNot(BeClosed()) - nowLock.Lock() - now = now.Add(5 * time.Millisecond) - nowLock.Unlock() - tick <- now + forwardQueueTimeBy(5 * time.Millisecond) Eventually(retrievedItem).Should(BeClosed()) Consistently(retrievedSecondItem).ShouldNot(BeClosed()) - nowLock.Lock() - now = now.Add(635 * time.Millisecond) - nowLock.Unlock() - tick <- now + forwardQueueTimeBy(635 * time.Millisecond) Eventually(retrievedSecondItem).Should(BeClosed()) Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) @@ -692,7 +652,31 @@ func TestFuzzPriorityQueue(t *testing.T) { wg.Wait() } -func newQueue() (PriorityQueue[string], *fakeMetricsProvider) { +func newQueueWithTimeForwarder() (_ *priorityqueue[string], _ *fakeMetricsProvider, forwardQueueTime func(time.Duration)) { + q, m := newQueue() + + now := time.Now().Round(time.Second) + nowLock := sync.Mutex{} + tick := make(chan time.Time) + + q.now = func() time.Time { + nowLock.Lock() + defer nowLock.Unlock() + return now + } + q.tick = func(d time.Duration) <-chan time.Time { + return tick + } + + return q, m, func(d time.Duration) { + nowLock.Lock() + now = now.Add(d) + nowLock.Unlock() + tick <- now + } +} + +func newQueue() (*priorityqueue[string], *fakeMetricsProvider) { metrics := newFakeMetricsProvider() q := New("test", func(o *Opts[string]) { o.MetricProvider = metrics @@ -710,7 +694,7 @@ func newQueue() (PriorityQueue[string], *fakeMetricsProvider) { } return upstreamTick(d) } - return q, metrics + return q.(*priorityqueue[string]), metrics } type btreeInteractionValidator struct { From c8a5a35177d3efc56918023f5253ffd57dc943ad Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sun, 5 Oct 2025 04:36:57 -0400 Subject: [PATCH 15/68] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Add=20subresource=20?= =?UTF-8?q?apply=20support=20(#3321)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Add subresource apply support * Revert "Revert deprecation of client.Apply" This reverts commit 6e1e8b2ee21a1c34989837047f0b84ca071700c7. * Fixups --- pkg/client/client.go | 34 +++++++ pkg/client/client_test.go | 120 ++++++++++++++++++++++- pkg/client/dryrun.go | 4 + pkg/client/dryrun_test.go | 33 +++++++ pkg/client/fake/client.go | 36 +++++++ pkg/client/fake/client_test.go | 43 ++++++-- pkg/client/fake/versioned_tracker.go | 9 +- pkg/client/fieldowner.go | 4 + pkg/client/fieldowner_test.go | 36 ++++++- pkg/client/fieldvalidation.go | 7 ++ pkg/client/fieldvalidation_test.go | 17 +++- pkg/client/interceptor/intercept.go | 8 ++ pkg/client/interceptor/intercept_test.go | 25 +++++ pkg/client/interfaces.go | 3 + pkg/client/namespaced_client.go | 49 ++++++--- pkg/client/namespaced_client_test.go | 43 ++++++++ pkg/client/options.go | 19 ++++ pkg/client/options_test.go | 18 ++++ pkg/client/patch.go | 5 +- pkg/client/typed_client.go | 33 +++++++ pkg/client/unstructured_client.go | 32 ++++++ 21 files changed, 543 insertions(+), 35 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 092deb43d4..7e38142273 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -544,6 +544,30 @@ func (po *SubResourcePatchOptions) ApplyToSubResourcePatch(o *SubResourcePatchOp } } +// SubResourceApplyOptions are the options for a subresource +// apply request. +type SubResourceApplyOptions struct { + ApplyOptions + SubResourceBody runtime.ApplyConfiguration +} + +// ApplyOpts applies the given options. +func (ao *SubResourceApplyOptions) ApplyOpts(opts []SubResourceApplyOption) *SubResourceApplyOptions { + for _, o := range opts { + o.ApplyToSubResourceApply(ao) + } + + return ao +} + +// ApplyToSubResourceApply applies the configuration on the given patch options. +func (ao *SubResourceApplyOptions) ApplyToSubResourceApply(o *SubResourceApplyOptions) { + ao.ApplyOptions.ApplyToApply(&o.ApplyOptions) + if ao.SubResourceBody != nil { + o.SubResourceBody = ao.SubResourceBody + } +} + func (sc *subResourceClient) Get(ctx context.Context, obj Object, subResource Object, opts ...SubResourceGetOption) error { switch obj.(type) { case runtime.Unstructured: @@ -595,3 +619,13 @@ func (sc *subResourceClient) Patch(ctx context.Context, obj Object, patch Patch, return sc.client.typedClient.PatchSubResource(ctx, obj, sc.subResource, patch, opts...) } } + +func (sc *subResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error { + switch obj := obj.(type) { + case *unstructuredApplyConfiguration: + defer sc.client.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind()) + return sc.client.unstructuredClient.ApplySubResource(ctx, obj, sc.subResource, opts...) + default: + return sc.client.typedClient.ApplySubResource(ctx, obj, sc.subResource, opts...) + } +} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index c775f28718..42e14771cc 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -43,6 +43,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1" + autoscaling1applyconfigurations "k8s.io/client-go/applyconfigurations/autoscaling/v1" corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" kscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -1127,6 +1129,34 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).NotTo(HaveOccurred()) Expect(*dep.Spec.Replicas).To(Equal(replicaCount)) }) + + It("should be able to apply the scale subresource", func(ctx SpecContext) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("Creating a deployment") + dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + replicaCount := *dep.Spec.Replicas + 1 + + By("Applying the scale subresurce") + deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "foo") + Expect(err).NotTo(HaveOccurred()) + scale := autoscaling1applyconfigurations.Scale(). + WithSpec(autoscaling1applyconfigurations.ScaleSpec().WithReplicas(replicaCount)) + err = cl.SubResource("scale").Apply(ctx, deploymentAC, + &client.SubResourceApplyOptions{SubResourceBody: scale}, + client.FieldOwner("foo"), + client.ForceOwnership, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Asserting replicas got updated") + dep, err = clientset.AppsV1().Deployments(dep.Namespace).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(*dep.Spec.Replicas).To(Equal(replicaCount)) + }) }) Context("with unstructured objects", func() { @@ -1322,8 +1352,8 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC By("Creating a deployment") dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) - dep.APIVersion = "apps/v1" - dep.Kind = "Deployment" + dep.APIVersion = appsv1.SchemeGroupVersion.String() + dep.Kind = "Deployment" //nolint:goconst depUnstructured, err := toUnstructured(dep) Expect(err).NotTo(HaveOccurred()) @@ -1374,6 +1404,41 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(err).NotTo(HaveOccurred()) Expect(*dep.Spec.Replicas).To(Equal(replicaCount)) }) + + It("should be able to apply the scale subresource", func(ctx SpecContext) { + cl, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("Creating a deployment") + dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + dep.APIVersion = "apps/v1" + dep.Kind = "Deployment" + depUnstructured, err := toUnstructured(dep) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the scale subresurce") + replicaCount := *dep.Spec.Replicas + 1 + scale := &unstructured.Unstructured{} + scale.SetAPIVersion("autoscaling/v1") + scale.SetKind("Scale") + Expect(unstructured.SetNestedField(scale.Object, int64(replicaCount), "spec", "replicas")).NotTo(HaveOccurred()) + err = cl.SubResource("scale").Apply(ctx, + client.ApplyConfigurationFromUnstructured(depUnstructured), + &client.SubResourceApplyOptions{SubResourceBody: client.ApplyConfigurationFromUnstructured(scale)}, + client.FieldOwner("foo"), + client.ForceOwnership, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(scale.GetAPIVersion()).To(Equal("autoscaling/v1")) + Expect(scale.GetKind()).To(Equal("Scale")) + + By("Asserting replicas got updated") + dep, err = clientset.AppsV1().Deployments(dep.Namespace).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(*dep.Spec.Replicas).To(Equal(replicaCount)) + }) }) }) @@ -1440,6 +1505,29 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(dep.GroupVersionKind()).To(Equal(depGvk)) }) + It("should apply status", func(ctx SpecContext) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(dep.Status.Replicas).To(BeEquivalentTo(0)) + + By("applying the status of Deployment") + deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "foo") + Expect(err).NotTo(HaveOccurred()) + deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{ + Replicas: ptr.To(int32(1)), + }) + Expect(cl.Status().Apply(ctx, deploymentAC, client.FieldOwner("foo"))).To(Succeed()) + + dep, err = clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(dep.Status.Replicas).To(BeEquivalentTo(1)) + }) + It("should not update spec of an existing object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) @@ -1592,6 +1680,34 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.Status.Replicas).To(BeEquivalentTo(1)) }) + It("should apply status and preserve type information", func(ctx SpecContext) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + + By("initially creating a Deployment") + dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(dep.Status.Replicas).To(BeEquivalentTo(0)) + + By("applying the status of Deployment") + dep.Status.Replicas = 1 + dep.ManagedFields = nil // Must be unset in SSA requests + u := &unstructured.Unstructured{} + Expect(scheme.Convert(dep, u, nil)).To(Succeed()) + err = cl.Status().Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner("foo")) + Expect(err).NotTo(HaveOccurred()) + + By("validating updated Deployment has type information") + Expect(u.GroupVersionKind()).To(Equal(depGvk)) + + By("validating patched Deployment has new status") + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.Status.Replicas).To(BeEquivalentTo(1)) + }) + It("should not update spec of an existing object", func(ctx SpecContext) { cl, err := client.New(cfg, client.Options{}) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/client/dryrun.go b/pkg/client/dryrun.go index a185860d33..fb7012200f 100644 --- a/pkg/client/dryrun.go +++ b/pkg/client/dryrun.go @@ -132,3 +132,7 @@ func (sw *dryRunSubResourceClient) Update(ctx context.Context, obj Object, opts func (sw *dryRunSubResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error { return sw.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...) } + +func (sw *dryRunSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error { + return sw.client.Apply(ctx, obj, append(opts, DryRunAll)...) +} diff --git a/pkg/client/dryrun_test.go b/pkg/client/dryrun_test.go index 912a4a10dc..35a9b63869 100644 --- a/pkg/client/dryrun_test.go +++ b/pkg/client/dryrun_test.go @@ -27,6 +27,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -260,4 +261,36 @@ var _ = Describe("DryRunClient", func() { Expect(actual).NotTo(BeNil()) Expect(actual).To(BeEquivalentTo(dep)) }) + + It("should not change objects via status apply", func(ctx SpecContext) { + deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "test-owner") + Expect(err).NotTo(HaveOccurred()) + deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{ + Replicas: ptr.To(int32(99)), + }) + + Expect(getClient().Status().Apply(ctx, deploymentAC, client.FieldOwner("test-owner"))).NotTo(HaveOccurred()) + + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual).To(BeEquivalentTo(dep)) + }) + + It("should not change objects via status apply with opts", func(ctx SpecContext) { + deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "test-owner") + Expect(err).NotTo(HaveOccurred()) + deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{ + Replicas: ptr.To(int32(99)), + }) + + opts := &client.SubResourceApplyOptions{ApplyOptions: client.ApplyOptions{DryRun: []string{"Bye", "Pippa"}}} + + Expect(getClient().Status().Apply(ctx, deploymentAC, client.FieldOwner("test-owner"), opts)).NotTo(HaveOccurred()) + + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual).To(BeEquivalentTo(dep)) + }) }) diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 41cf233deb..62067cb19c 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -1335,6 +1335,42 @@ func (sw *fakeSubResourceClient) statusPatch(body client.Object, patch client.Pa return sw.client.patch(body, patch, &patchOptions.PatchOptions) } +func (sw *fakeSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { + if sw.subResource != "status" { + return errors.New("fakeSubResourceClient currently only supports Apply for status subresource") + } + + applyOpts := &client.SubResourceApplyOptions{} + applyOpts.ApplyOpts(opts) + + data, err := json.Marshal(obj) + if err != nil { + return fmt.Errorf("failed to marshal apply configuration: %w", err) + } + + u := &unstructured.Unstructured{} + if err := json.Unmarshal(data, u); err != nil { + return fmt.Errorf("failed to unmarshal apply configuration: %w", err) + } + + patchOpts := &client.SubResourcePatchOptions{} + patchOpts.Raw = applyOpts.AsPatchOptions() + + if applyOpts.SubResourceBody != nil { + subResourceBodySerialized, err := json.Marshal(applyOpts.SubResourceBody) + if err != nil { + return fmt.Errorf("failed to serialize subresource body: %w", err) + } + subResourceBody := &unstructured.Unstructured{} + if err := json.Unmarshal(subResourceBodySerialized, subResourceBody); err != nil { + return fmt.Errorf("failed to unmarshal subresource body: %w", err) + } + patchOpts.SubResourceBody = subResourceBody + } + + return sw.Patch(ctx, u, &fakeApplyPatch{}, patchOpts) +} + func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool { switch gvk.Group { case "apps": diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 23f52b9fb8..36722b4ddc 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -1809,6 +1809,31 @@ var _ = Describe("Fake client", func() { Expect(pod.Status).To(BeComparableTo(corev1.PodStatus{})) }) + It("should only change status on status apply", func(ctx SpecContext) { + initial := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node", + }, + Spec: corev1.NodeSpec{ + PodCIDR: "old-cidr", + }, + } + cl := NewClientBuilder().WithStatusSubresource(&corev1.Node{}).WithObjects(initial).Build() + + ac := corev1applyconfigurations.Node(initial.Name). + WithSpec(corev1applyconfigurations.NodeSpec().WithPodCIDR(initial.Spec.PodCIDR + "-updated")). + WithStatus(corev1applyconfigurations.NodeStatus().WithPhase(corev1.NodeRunning)) + + Expect(cl.Status().Apply(ctx, ac, client.FieldOwner("test-owner"))).To(Succeed()) + + actual := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: initial.Name}} + Expect(cl.Get(ctx, client.ObjectKeyFromObject(actual), actual)).To(Succeed()) + + initial.ResourceVersion = actual.ResourceVersion + initial.Status = actual.Status + Expect(initial).To(BeComparableTo(actual)) + }) + It("should Unmarshal the schemaless object with int64 to preserve ints", func(ctx SpecContext) { schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "test", Version: "v1"}} schemeBuilder.Register(&WithSchemalessSpec{}) @@ -2694,7 +2719,7 @@ var _ = Describe("Fake client", func() { obj.SetName("foo") Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} @@ -2702,7 +2727,7 @@ var _ = Describe("Fake client", func() { Expect(cm.Data).To(Equal(map[string]string{"some": "data"})) Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) Expect(cm.Data).To(Equal(map[string]string{"other": "data"})) @@ -2718,13 +2743,13 @@ var _ = Describe("Fake client", func() { Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "spec")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) Expect(result.Object["spec"]).To(Equal(map[string]any{"some": "data"})) Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "spec")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) @@ -2738,9 +2763,9 @@ var _ = Describe("Fake client", func() { obj.SetName("foo") Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed - err := cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo")) + err := cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo")) //nolint:staticcheck // will be removed once client.Apply is removed Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("metadata.managedFields must be nil")) }) @@ -2756,7 +2781,7 @@ var _ = Describe("Fake client", func() { Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} @@ -2764,7 +2789,7 @@ var _ = Describe("Fake client", func() { Expect(cm.Data).To(Equal(map[string]string{"some": "data"})) Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "data")).To(Succeed()) - Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) + Expect(cl.Patch(ctx, obj, client.Apply, client.FieldOwner("foo"))).To(Succeed()) //nolint:staticcheck // will be removed once client.Apply is removed Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) Expect(cm.Data).To(Equal(map[string]string{"other": "data"})) @@ -2810,7 +2835,7 @@ var _ = Describe("Fake client", func() { "ssa": "value", }, }} - Expect(cl.Patch(ctx, u, client.Apply, client.FieldOwner("foo"))).NotTo(HaveOccurred()) + Expect(cl.Patch(ctx, u, client.Apply, client.FieldOwner("foo"))).NotTo(HaveOccurred()) //nolint:staticcheck // will be removed once client.Apply is removed _, exists, err := unstructured.NestedFieldNoCopy(u.Object, "metadata", "managedFields") Expect(err).NotTo(HaveOccurred()) Expect(exists).To(BeTrue()) diff --git a/pkg/client/fake/versioned_tracker.go b/pkg/client/fake/versioned_tracker.go index c1caa1ca02..bc1eaeb951 100644 --- a/pkg/client/fake/versioned_tracker.go +++ b/pkg/client/fake/versioned_tracker.go @@ -307,7 +307,9 @@ func (t versionedTracker) Apply(gvr schema.GroupVersionResource, applyConfigurat if err != nil { return err } - applyConfiguration, needsCreate, err := t.updateObject(gvr, gvk, applyConfiguration, ns, false, false, true, patchOptions.DryRun) + isStatus := bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeSubResourceClient).statusPatch")) + + applyConfiguration, needsCreate, err := t.updateObject(gvr, gvk, applyConfiguration, ns, isStatus, false, true, patchOptions.DryRun) if err != nil { return err } @@ -334,6 +336,11 @@ func (t versionedTracker) Apply(gvr schema.GroupVersionResource, applyConfigurat return nil } + if isStatus { + // We restore everything but status from the tracker where we don't put GVK + // into the object but it must be set for the ManagedFieldsObjectTracker + applyConfiguration.GetObjectKind().SetGroupVersionKind(gvk) + } return t.upstream.Apply(gvr, applyConfiguration, ns, opts...) } diff --git a/pkg/client/fieldowner.go b/pkg/client/fieldowner.go index 93274f9500..5d9437ba91 100644 --- a/pkg/client/fieldowner.go +++ b/pkg/client/fieldowner.go @@ -108,3 +108,7 @@ func (f *subresourceClientWithFieldOwner) Update(ctx context.Context, obj Object func (f *subresourceClientWithFieldOwner) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error { return f.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{FieldOwner(f.owner)}, opts...)...) } + +func (f *subresourceClientWithFieldOwner) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error { + return f.subresourceWriter.Apply(ctx, obj, append([]SubResourceApplyOption{FieldOwner(f.owner)}, opts...)...) +} diff --git a/pkg/client/fieldowner_test.go b/pkg/client/fieldowner_test.go index 95cb4e0f91..069abbc115 100644 --- a/pkg/client/fieldowner_test.go +++ b/pkg/client/fieldowner_test.go @@ -21,6 +21,8 @@ import ( "testing" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" @@ -33,18 +35,22 @@ func TestWithFieldOwner(t *testing.T) { ctx := t.Context() dummyObj := &corev1.Namespace{} + dummyObjectAC := corev1applyconfigurations.Namespace(dummyObj.Name) _ = wrappedClient.Create(ctx, dummyObj) _ = wrappedClient.Update(ctx, dummyObj) _ = wrappedClient.Patch(ctx, dummyObj, nil) + _ = wrappedClient.Apply(ctx, dummyObjectAC) _ = wrappedClient.Status().Create(ctx, dummyObj, dummyObj) _ = wrappedClient.Status().Update(ctx, dummyObj) _ = wrappedClient.Status().Patch(ctx, dummyObj, nil) + _ = wrappedClient.Status().Apply(ctx, dummyObjectAC) _ = wrappedClient.SubResource("some-subresource").Create(ctx, dummyObj, dummyObj) _ = wrappedClient.SubResource("some-subresource").Update(ctx, dummyObj) _ = wrappedClient.SubResource("some-subresource").Patch(ctx, dummyObj, nil) + _ = wrappedClient.SubResource("some-subresource").Apply(ctx, dummyObjectAC) - if expectedCalls := 9; calls != expectedCalls { + if expectedCalls := 12; calls != expectedCalls { t.Fatalf("wrong number of calls to assertions: expected=%d; got=%d", expectedCalls, calls) } } @@ -57,18 +63,22 @@ func TestWithFieldOwnerOverridden(t *testing.T) { ctx := t.Context() dummyObj := &corev1.Namespace{} + dummyObjectAC := corev1applyconfigurations.Namespace(dummyObj.Name) _ = wrappedClient.Create(ctx, dummyObj, client.FieldOwner("new-field-manager")) _ = wrappedClient.Update(ctx, dummyObj, client.FieldOwner("new-field-manager")) _ = wrappedClient.Patch(ctx, dummyObj, nil, client.FieldOwner("new-field-manager")) + _ = wrappedClient.Apply(ctx, dummyObjectAC, client.FieldOwner("new-field-manager")) _ = wrappedClient.Status().Create(ctx, dummyObj, dummyObj, client.FieldOwner("new-field-manager")) _ = wrappedClient.Status().Update(ctx, dummyObj, client.FieldOwner("new-field-manager")) _ = wrappedClient.Status().Patch(ctx, dummyObj, nil, client.FieldOwner("new-field-manager")) + _ = wrappedClient.Status().Apply(ctx, dummyObjectAC, client.FieldOwner("new-field-manager")) _ = wrappedClient.SubResource("some-subresource").Create(ctx, dummyObj, dummyObj, client.FieldOwner("new-field-manager")) _ = wrappedClient.SubResource("some-subresource").Update(ctx, dummyObj, client.FieldOwner("new-field-manager")) _ = wrappedClient.SubResource("some-subresource").Patch(ctx, dummyObj, nil, client.FieldOwner("new-field-manager")) + _ = wrappedClient.SubResource("some-subresource").Apply(ctx, dummyObjectAC, client.FieldOwner("new-field-manager")) - if expectedCalls := 9; calls != expectedCalls { + if expectedCalls := 12; calls != expectedCalls { t.Fatalf("wrong number of calls to assertions: expected=%d; got=%d", expectedCalls, calls) } } @@ -144,5 +154,27 @@ func testClient(t *testing.T, expectedFieldManager string, callback func()) clie } return nil }, + Apply: func(ctx context.Context, c client.WithWatch, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + callback() + out := &client.ApplyOptions{} + for _, f := range opts { + f.ApplyToApply(out) + } + if got := out.FieldManager; expectedFieldManager != got { + t.Fatalf("wrong field manager: expected=%q; got=%q", expectedFieldManager, got) + } + return nil + }, + SubResourceApply: func(ctx context.Context, c client.Client, subResourceName string, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { + callback() + out := &client.SubResourceApplyOptions{} + for _, f := range opts { + f.ApplyToSubResourceApply(out) + } + if got := out.FieldManager; expectedFieldManager != got { + t.Fatalf("wrong field manager: expected=%q; got=%q", expectedFieldManager, got) + } + return nil + }, }).Build() } diff --git a/pkg/client/fieldvalidation.go b/pkg/client/fieldvalidation.go index ce8d0576c7..b0f660854e 100644 --- a/pkg/client/fieldvalidation.go +++ b/pkg/client/fieldvalidation.go @@ -27,6 +27,9 @@ import ( // WithFieldValidation wraps a Client and configures field validation, by // default, for all write requests from this client. Users can override field // validation for individual write requests. +// +// This wrapper has no effect on apply requests, as they do not support a +// custom fieldValidation setting, it is always strict. func WithFieldValidation(c Client, validation FieldValidation) Client { return &clientWithFieldValidation{ validation: validation, @@ -108,3 +111,7 @@ func (c *subresourceClientWithFieldValidation) Update(ctx context.Context, obj O func (c *subresourceClientWithFieldValidation) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error { return c.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{c.validation}, opts...)...) } + +func (c *subresourceClientWithFieldValidation) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error { + return c.subresourceWriter.Apply(ctx, obj, opts...) +} diff --git a/pkg/client/fieldvalidation_test.go b/pkg/client/fieldvalidation_test.go index d32ee5717d..6e6e9e5d17 100644 --- a/pkg/client/fieldvalidation_test.go +++ b/pkg/client/fieldvalidation_test.go @@ -92,6 +92,15 @@ var _ = Describe("ClientWithFieldValidation", func() { err = wrappedClient.SubResource("status").Patch(ctx, invalidStatusNode, patch) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("strict decoding error: unknown field \"status.invalidStatusField\"")) + + invalidApplyConfig := client.ApplyConfigurationFromUnstructured(invalidStatusNode) + err = wrappedClient.Status().Apply(ctx, invalidApplyConfig, client.FieldOwner("test-owner")) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("field not declared in schema")) + + err = wrappedClient.SubResource("status").Apply(ctx, invalidApplyConfig, client.FieldOwner("test-owner")) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("field not declared in schema")) }) }) @@ -110,11 +119,13 @@ func TestWithStrictFieldValidation(t *testing.T) { _ = wrappedClient.Status().Create(ctx, dummyObj, dummyObj) _ = wrappedClient.Status().Update(ctx, dummyObj) _ = wrappedClient.Status().Patch(ctx, dummyObj, nil) + _ = wrappedClient.Status().Apply(ctx, corev1applyconfigurations.Namespace(""), nil) _ = wrappedClient.SubResource("some-subresource").Create(ctx, dummyObj, dummyObj) _ = wrappedClient.SubResource("some-subresource").Update(ctx, dummyObj) _ = wrappedClient.SubResource("some-subresource").Patch(ctx, dummyObj, nil) + _ = wrappedClient.SubResource("some-subresource").Apply(ctx, corev1applyconfigurations.Namespace(""), nil) - if expectedCalls := 10; calls != expectedCalls { + if expectedCalls := 12; calls != expectedCalls { t.Fatalf("wrong number of calls to assertions: expected=%d; got=%d", expectedCalls, calls) } } @@ -278,5 +289,9 @@ func testFieldValidationClient(t *testing.T, expectedFieldValidation string, cal } return nil }, + SubResourceApply: func(ctx context.Context, c client.Client, subResourceName string, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { + callback() + return nil + }, }).Build() } diff --git a/pkg/client/interceptor/intercept.go b/pkg/client/interceptor/intercept.go index 7ff73bd8da..b98af1a693 100644 --- a/pkg/client/interceptor/intercept.go +++ b/pkg/client/interceptor/intercept.go @@ -26,6 +26,7 @@ type Funcs struct { SubResourceCreate func(ctx context.Context, client client.Client, subResourceName string, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error SubResourceUpdate func(ctx context.Context, client client.Client, subResourceName string, obj client.Object, opts ...client.SubResourceUpdateOption) error SubResourcePatch func(ctx context.Context, client client.Client, subResourceName string, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error + SubResourceApply func(ctx context.Context, client client.Client, subResourceName string, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error } // NewClient returns a new interceptor client that calls the functions in funcs instead of the underlying client's methods, if they are not nil. @@ -173,3 +174,10 @@ func (s subResourceInterceptor) Patch(ctx context.Context, obj client.Object, pa } return s.client.SubResource(s.subResourceName).Patch(ctx, obj, patch, opts...) } + +func (s subResourceInterceptor) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { + if s.funcs.SubResourceApply != nil { + return s.funcs.SubResourceApply(ctx, s.client, s.subResourceName, obj, opts...) + } + return s.client.SubResource(s.subResourceName).Apply(ctx, obj, opts...) +} diff --git a/pkg/client/interceptor/intercept_test.go b/pkg/client/interceptor/intercept_test.go index 26ea5b057e..fb58dfeac1 100644 --- a/pkg/client/interceptor/intercept_test.go +++ b/pkg/client/interceptor/intercept_test.go @@ -351,6 +351,31 @@ var _ = Describe("NewSubResourceClient", func() { _ = client2.SubResource("foo").Create(ctx, nil, nil) Expect(called).To(BeTrue()) }) + It("should call the provided Apply function", func(ctx SpecContext) { + var called bool + client := NewClient(c, Funcs{ + SubResourceApply: func(_ context.Context, client client.Client, subResourceName string, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { + called = true + Expect(subResourceName).To(BeEquivalentTo("foo")) + return nil + }, + }) + _ = client.SubResource("foo").Apply(ctx, nil) + Expect(called).To(BeTrue()) + }) + It("should call the underlying client if the provided Apply function is nil", func(ctx SpecContext) { + var called bool + client1 := NewClient(c, Funcs{ + SubResourceApply: func(_ context.Context, client client.Client, subResourceName string, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { + called = true + Expect(subResourceName).To(BeEquivalentTo("foo")) + return nil + }, + }) + client2 := NewClient(client1, Funcs{}) + _ = client2.SubResource("foo").Apply(ctx, nil) + Expect(called).To(BeTrue()) + }) }) type dummyClient struct{} diff --git a/pkg/client/interfaces.go b/pkg/client/interfaces.go index 61559ecbe1..1af1f3a368 100644 --- a/pkg/client/interfaces.go +++ b/pkg/client/interfaces.go @@ -155,6 +155,9 @@ type SubResourceWriter interface { // pointer so that obj can be updated with the content returned by the // Server. Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error + + // Apply applies the given apply configurations subresource. + Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error } // SubResourceClient knows how to perform CRU operations on Kubernetes objects. diff --git a/pkg/client/namespaced_client.go b/pkg/client/namespaced_client.go index cacba4a9c6..445e91b98b 100644 --- a/pkg/client/namespaced_client.go +++ b/pkg/client/namespaced_client.go @@ -150,7 +150,7 @@ func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, o return n.client.Patch(ctx, obj, patch, opts...) } -func (n *namespacedClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { +func (n *namespacedClient) setNamespaceForApplyConfigIfNamespaceScoped(obj runtime.ApplyConfiguration) error { var gvk schema.GroupVersionKind switch o := obj.(type) { case applyConfiguration: @@ -193,6 +193,14 @@ func (n *namespacedClient) Apply(ctx context.Context, obj runtime.ApplyConfigura } } + return nil +} + +func (n *namespacedClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + if err := n.setNamespaceForApplyConfigIfNamespaceScoped(obj); err != nil { + return err + } + return n.client.Apply(ctx, obj, opts...) } @@ -226,7 +234,10 @@ func (n *namespacedClient) Status() SubResourceWriter { // SubResource implements client.SubResourceClient. func (n *namespacedClient) SubResource(subResource string) SubResourceClient { - return &namespacedClientSubResourceClient{client: n.client.SubResource(subResource), namespace: n.namespace, namespacedclient: n} + return &namespacedClientSubResourceClient{ + client: n.client.SubResource(subResource), + namespacedclient: n, + } } // ensure namespacedClientSubResourceClient implements client.SubResourceClient. @@ -234,8 +245,7 @@ var _ SubResourceClient = &namespacedClientSubResourceClient{} type namespacedClientSubResourceClient struct { client SubResourceClient - namespace string - namespacedclient Client + namespacedclient *namespacedClient } func (nsw *namespacedClientSubResourceClient) Get(ctx context.Context, obj, subResource Object, opts ...SubResourceGetOption) error { @@ -245,12 +255,12 @@ func (nsw *namespacedClientSubResourceClient) Get(ctx context.Context, obj, subR } objectNamespace := obj.GetNamespace() - if objectNamespace != nsw.namespace && objectNamespace != "" { - return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace) + if objectNamespace != nsw.namespacedclient.namespace && objectNamespace != "" { + return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespacedclient.namespace) } if isNamespaceScoped && objectNamespace == "" { - obj.SetNamespace(nsw.namespace) + obj.SetNamespace(nsw.namespacedclient.namespace) } return nsw.client.Get(ctx, obj, subResource, opts...) @@ -263,12 +273,12 @@ func (nsw *namespacedClientSubResourceClient) Create(ctx context.Context, obj, s } objectNamespace := obj.GetNamespace() - if objectNamespace != nsw.namespace && objectNamespace != "" { - return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace) + if objectNamespace != nsw.namespacedclient.namespace && objectNamespace != "" { + return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespacedclient.namespace) } if isNamespaceScoped && objectNamespace == "" { - obj.SetNamespace(nsw.namespace) + obj.SetNamespace(nsw.namespacedclient.namespace) } return nsw.client.Create(ctx, obj, subResource, opts...) @@ -282,12 +292,12 @@ func (nsw *namespacedClientSubResourceClient) Update(ctx context.Context, obj Ob } objectNamespace := obj.GetNamespace() - if objectNamespace != nsw.namespace && objectNamespace != "" { - return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace) + if objectNamespace != nsw.namespacedclient.namespace && objectNamespace != "" { + return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespacedclient.namespace) } if isNamespaceScoped && objectNamespace == "" { - obj.SetNamespace(nsw.namespace) + obj.SetNamespace(nsw.namespacedclient.namespace) } return nsw.client.Update(ctx, obj, opts...) } @@ -300,12 +310,19 @@ func (nsw *namespacedClientSubResourceClient) Patch(ctx context.Context, obj Obj } objectNamespace := obj.GetNamespace() - if objectNamespace != nsw.namespace && objectNamespace != "" { - return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace) + if objectNamespace != nsw.namespacedclient.namespace && objectNamespace != "" { + return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespacedclient.namespace) } if isNamespaceScoped && objectNamespace == "" { - obj.SetNamespace(nsw.namespace) + obj.SetNamespace(nsw.namespacedclient.namespace) } return nsw.client.Patch(ctx, obj, patch, opts...) } + +func (nsw *namespacedClientSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error { + if err := nsw.namespacedclient.setNamespaceForApplyConfigIfNamespaceScoped(obj); err != nil { + return err + } + return nsw.client.Apply(ctx, obj, opts...) +} diff --git a/pkg/client/namespaced_client_test.go b/pkg/client/namespaced_client_test.go index cf28289e72..deae881d4a 100644 --- a/pkg/client/namespaced_client_test.go +++ b/pkg/client/namespaced_client_test.go @@ -37,6 +37,7 @@ import ( corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" metav1applyconfigurations "k8s.io/client-go/applyconfigurations/meta/v1" rbacv1applyconfigurations "k8s.io/client-go/applyconfigurations/rbac/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -613,6 +614,48 @@ var _ = Describe("NamespacedClient", func() { Expect(getClient().SubResource("status").Patch(ctx, changedDep, client.MergeFrom(dep))).To(HaveOccurred()) }) + + It("should change objects via status apply", func(ctx SpecContext) { + deploymentAC, err := appsv1applyconfigurations.ExtractDeployment(dep, "test-owner") + Expect(err).NotTo(HaveOccurred()) + deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{ + Replicas: ptr.To(int32(99)), + }) + + Expect(getClient().SubResource("status").Apply(ctx, deploymentAC, client.FieldOwner("test-owner"))).To(Succeed()) + + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.GetNamespace()).To(BeEquivalentTo(ns)) + Expect(actual.Status.Replicas).To(BeEquivalentTo(99)) + }) + + It("should set namespace on ApplyConfiguration when applying via SubResource", func(ctx SpecContext) { + deploymentAC := appsv1applyconfigurations.Deployment(dep.Name, "") + deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{ + Replicas: ptr.To(int32(50)), + }) + + Expect(getClient().SubResource("status").Apply(ctx, deploymentAC, client.FieldOwner("test-owner"))).To(Succeed()) + + actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual).NotTo(BeNil()) + Expect(actual.GetNamespace()).To(BeEquivalentTo(ns)) + Expect(actual.Status.Replicas).To(BeEquivalentTo(50)) + }) + + It("should fail when applying via SubResource with conflicting namespace", func(ctx SpecContext) { + deploymentAC := appsv1applyconfigurations.Deployment(dep.Name, "different-namespace") + deploymentAC.WithStatus(&appsv1applyconfigurations.DeploymentStatusApplyConfiguration{ + Replicas: ptr.To(int32(25)), + }) + + err := getClient().SubResource("status").Apply(ctx, deploymentAC, client.FieldOwner("test-owner")) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("namespace")) + }) }) Describe("Test on invalid objects", func() { diff --git a/pkg/client/options.go b/pkg/client/options.go index 33c460738c..a6b921171a 100644 --- a/pkg/client/options.go +++ b/pkg/client/options.go @@ -97,6 +97,12 @@ type SubResourcePatchOption interface { ApplyToSubResourcePatch(*SubResourcePatchOptions) } +// SubResourceApplyOption configures a subresource apply request. +type SubResourceApplyOption interface { + // ApplyToSubResourceApply applies the configuration on the given patch options. + ApplyToSubResourceApply(*SubResourceApplyOptions) +} + // }}} // {{{ Multi-Type Options @@ -148,6 +154,10 @@ func (dryRunAll) ApplyToSubResourcePatch(opts *SubResourcePatchOptions) { opts.DryRun = []string{metav1.DryRunAll} } +func (dryRunAll) ApplyToSubResourceApply(opts *SubResourceApplyOptions) { + opts.DryRun = []string{metav1.DryRunAll} +} + // FieldOwner set the field manager name for the given server-side apply patch. type FieldOwner string @@ -186,6 +196,11 @@ func (f FieldOwner) ApplyToSubResourceUpdate(opts *SubResourceUpdateOptions) { opts.FieldManager = string(f) } +// ApplyToSubResourceApply applies this configuration to the given apply options. +func (f FieldOwner) ApplyToSubResourceApply(opts *SubResourceApplyOptions) { + opts.FieldManager = string(f) +} + // FieldValidation configures field validation for the given requests. type FieldValidation string @@ -949,6 +964,10 @@ func (forceOwnership) ApplyToApply(opts *ApplyOptions) { opts.Force = ptr.To(true) } +func (forceOwnership) ApplyToSubResourceApply(opts *SubResourceApplyOptions) { + opts.Force = ptr.To(true) +} + // }}} // {{{ DeleteAllOf Options diff --git a/pkg/client/options_test.go b/pkg/client/options_test.go index 0aa6a74007..082586bca3 100644 --- a/pkg/client/options_test.go +++ b/pkg/client/options_test.go @@ -374,6 +374,12 @@ var _ = Describe("DryRunAll", func() { t.ApplyToSubResourceUpdate(o) Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) }) + It("Should apply to SubResourceApplyOptions", func() { + o := &client.SubResourceApplyOptions{ApplyOptions: client.ApplyOptions{DryRun: []string{"server"}}} + t := client.DryRunAll + t.ApplyToSubResourceApply(o) + Expect(o.DryRun).To(Equal([]string{metav1.DryRunAll})) + }) }) var _ = Describe("FieldOwner", func() { @@ -419,6 +425,12 @@ var _ = Describe("FieldOwner", func() { t.ApplyToSubResourceUpdate(o) Expect(o.FieldManager).To(Equal("foo")) }) + It("Should apply to SubResourceApplyOptions", func() { + o := &client.SubResourceApplyOptions{ApplyOptions: client.ApplyOptions{FieldManager: "bar"}} + t := client.FieldOwner("foo") + t.ApplyToSubResourceApply(o) + Expect(o.FieldManager).To(Equal("foo")) + }) }) var _ = Describe("ForceOwnership", func() { @@ -440,6 +452,12 @@ var _ = Describe("ForceOwnership", func() { t.ApplyToApply(o) Expect(*o.Force).To(BeTrue()) }) + It("Should apply to SubResourceApplyOptions", func() { + o := &client.SubResourceApplyOptions{} + t := client.ForceOwnership + t.ApplyToSubResourceApply(o) + Expect(*o.Force).To(BeTrue()) + }) }) var _ = Describe("HasLabels", func() { diff --git a/pkg/client/patch.go b/pkg/client/patch.go index b99d7663bd..9bd0953fdc 100644 --- a/pkg/client/patch.go +++ b/pkg/client/patch.go @@ -28,10 +28,7 @@ import ( var ( // Apply uses server-side apply to patch the given object. // - // This should now only be used to patch sub resources, e.g. with client.Client.Status().Patch(). - // Use client.Client.Apply() instead of client.Client.Patch(..., client.Apply, ...) - // This will be deprecated once the Apply method has been added for sub resources. - // See the following issue for more details: https://github.com/kubernetes-sigs/controller-runtime/issues/3183 + // Deprecated: Use client.Client.Apply() and client.Client.SubResource("subrsource").Apply() instead. Apply Patch = applyPatch{} // Merge uses the raw object as a merge patch, without modifications. diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index 3bd762a638..66ae2e4a5c 100644 --- a/pkg/client/typed_client.go +++ b/pkg/client/typed_client.go @@ -304,3 +304,36 @@ func (c *typedClient) PatchSubResource(ctx context.Context, obj Object, subResou Do(ctx). Into(body) } + +func (c *typedClient) ApplySubResource(ctx context.Context, obj runtime.ApplyConfiguration, subResource string, opts ...SubResourceApplyOption) error { + o, err := c.resources.getObjMeta(obj) + if err != nil { + return err + } + + applyOpts := &SubResourceApplyOptions{} + applyOpts.ApplyOpts(opts) + + body := obj + if applyOpts.SubResourceBody != nil { + body = applyOpts.SubResourceBody + } + + req, err := apply.NewRequest(o, body) + if err != nil { + return fmt.Errorf("failed to create apply request: %w", err) + } + + return req. + NamespaceIfScoped(o.namespace, o.isNamespaced()). + Resource(o.resource()). + Name(o.name). + SubResource(subResource). + VersionedParams(applyOpts.AsPatchOptions(), c.paramCodec). + Do(ctx). + // This is hacky, it is required because `Into` takes a `runtime.Object` and + // that is not implemented by the ApplyConfigurations. The generated clients + // don't have this problem because they deserialize into the api type, not the + // apply configuration: https://github.com/kubernetes/kubernetes/blob/22f5e01a37c0bc6a5f494dec14dd4e3688ee1d55/staging/src/k8s.io/client-go/gentype/type.go#L296-L317 + Into(runtimeObjectFromApplyConfiguration(obj)) +} diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index e636c3beef..d2ea6d7a32 100644 --- a/pkg/client/unstructured_client.go +++ b/pkg/client/unstructured_client.go @@ -386,3 +386,35 @@ func (uc *unstructuredClient) PatchSubResource(ctx context.Context, obj Object, u.GetObjectKind().SetGroupVersionKind(gvk) return result } + +func (uc *unstructuredClient) ApplySubResource(ctx context.Context, obj runtime.ApplyConfiguration, subResource string, opts ...SubResourceApplyOption) error { + unstructuredApplyConfig, ok := obj.(*unstructuredApplyConfiguration) + if !ok { + return fmt.Errorf("bug: unstructured client got an applyconfiguration that was not %T but %T", &unstructuredApplyConfiguration{}, obj) + } + o, err := uc.resources.getObjMeta(unstructuredApplyConfig.Unstructured) + if err != nil { + return err + } + + applyOpts := &SubResourceApplyOptions{} + applyOpts.ApplyOpts(opts) + + body := obj + if applyOpts.SubResourceBody != nil { + body = applyOpts.SubResourceBody + } + req, err := apply.NewRequest(o, body) + if err != nil { + return fmt.Errorf("failed to create apply request: %w", err) + } + + return req. + NamespaceIfScoped(o.namespace, o.isNamespaced()). + Resource(o.resource()). + Name(o.name). + SubResource(subResource). + VersionedParams(applyOpts.AsPatchOptions(), uc.paramCodec). + Do(ctx). + Into(unstructuredApplyConfig.Unstructured) +} From c71b8c8fcf41f9661f007fd74f1766d2cfa938ef Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Fri, 3 Oct 2025 08:43:07 +0200 Subject: [PATCH 16/68] Add Priority field to reconcile.Result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- pkg/internal/controller/controller.go | 7 +- pkg/internal/controller/controller_test.go | 91 +++++++++++++++++++++- pkg/reconcile/reconcile.go | 5 ++ 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/pkg/internal/controller/controller.go b/pkg/internal/controller/controller.go index ea79681862..7dd06957eb 100644 --- a/pkg/internal/controller/controller.go +++ b/pkg/internal/controller/controller.go @@ -459,6 +459,9 @@ func (c *Controller[request]) reconcileHandler(ctx context.Context, req request, // resource to be synced. log.V(5).Info("Reconciling") result, err := c.Reconcile(ctx, req) + if result.Priority != nil { + priority = *result.Priority + } switch { case err != nil: if errors.Is(err, reconcile.TerminalError(nil)) { @@ -468,8 +471,8 @@ func (c *Controller[request]) reconcileHandler(ctx context.Context, req request, } ctrlmetrics.ReconcileErrors.WithLabelValues(c.Name).Inc() ctrlmetrics.ReconcileTotal.WithLabelValues(c.Name, labelError).Inc() - if !result.IsZero() { - log.Info("Warning: Reconciler returned both a non-zero result and a non-nil error. The result will always be ignored if the error is non-nil and the non-nil error causes requeuing with exponential backoff. For more details, see: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile#Reconciler") + if result.RequeueAfter > 0 || result.Requeue { //nolint: staticcheck // We have to handle Requeue until it is removed + log.Info("Warning: Reconciler returned both a result with either RequeueAfter or Requeue set and a non-nil error. RequeueAfter and Requeue will always be ignored if the error is non-nil. For more details, see: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile#Reconciler") } log.Error(err, "Reconciler error") case result.RequeueAfter > 0: diff --git a/pkg/internal/controller/controller_test.go b/pkg/internal/controller/controller_test.go index 306e0b0126..6d62b80e22 100644 --- a/pkg/internal/controller/controller_test.go +++ b/pkg/internal/controller/controller_test.go @@ -745,7 +745,36 @@ var _ = Describe("controller", func() { }})) }) - It("should requeue a Request after a duration (but not rate-limitted) if the Result sets RequeueAfter (regardless of Requeue)", func(ctx SpecContext) { + It("should use the priority from Result when the reconciler requests a requeue", func(ctx SpecContext) { + q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")} + ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { + return q + } + + go func() { + defer GinkgoRecover() + Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) + }() + + q.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: ptr.To(10)}, request) + + By("Invoking Reconciler which will request a requeue") + fakeReconcile.AddResult(reconcile.Result{Requeue: true, Priority: ptr.To(99)}, nil) + Expect(<-reconciled).To(Equal(request)) + Eventually(func() []priorityQueueAddition { + q.lock.Lock() + defer q.lock.Unlock() + return q.added + }).Should(Equal([]priorityQueueAddition{{ + AddOpts: priorityqueue.AddOpts{ + RateLimited: true, + Priority: ptr.To(99), + }, + items: []reconcile.Request{request}, + }})) + }) + + It("should requeue a Request after a duration (but not rate-limited) if the Result sets RequeueAfter (regardless of Requeue)", func(ctx SpecContext) { dq := &DelegatingQueue{TypedRateLimitingInterface: ctrl.NewQueue("controller1", nil)} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return dq @@ -775,7 +804,7 @@ var _ = Describe("controller", func() { Eventually(func() int { return dq.NumRequeues(request) }).Should(Equal(0)) }) - It("should retain the priority with RequeAfter", func(ctx SpecContext) { + It("should retain the priority with RequeueAfter", func(ctx SpecContext) { q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { return q @@ -804,6 +833,35 @@ var _ = Describe("controller", func() { }})) }) + It("should use the priority from Result with RequeueAfter", func(ctx SpecContext) { + q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")} + ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { + return q + } + + go func() { + defer GinkgoRecover() + Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) + }() + + q.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: ptr.To(10)}, request) + + By("Invoking Reconciler which will ask for RequeueAfter") + fakeReconcile.AddResult(reconcile.Result{RequeueAfter: time.Millisecond * 100, Priority: ptr.To(99)}, nil) + Expect(<-reconciled).To(Equal(request)) + Eventually(func() []priorityQueueAddition { + q.lock.Lock() + defer q.lock.Unlock() + return q.added + }).Should(Equal([]priorityQueueAddition{{ + AddOpts: priorityqueue.AddOpts{ + After: time.Millisecond * 100, + Priority: ptr.To(99), + }, + items: []reconcile.Request{request}, + }})) + }) + It("should perform error behavior if error is not nil, regardless of RequeueAfter", func(ctx SpecContext) { dq := &DelegatingQueue{TypedRateLimitingInterface: ctrl.NewQueue("controller1", nil)} ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { @@ -862,6 +920,35 @@ var _ = Describe("controller", func() { }})) }) + It("should use the priority from Result when there was an error", func(ctx SpecContext) { + q := &fakePriorityQueue{PriorityQueue: priorityqueue.New[reconcile.Request]("controller1")} + ctrl.NewQueue = func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { + return q + } + + go func() { + defer GinkgoRecover() + Expect(ctrl.Start(ctx)).NotTo(HaveOccurred()) + }() + + q.PriorityQueue.AddWithOpts(priorityqueue.AddOpts{Priority: ptr.To(10)}, request) + + By("Invoking Reconciler which will return an error") + fakeReconcile.AddResult(reconcile.Result{Priority: ptr.To(99)}, errors.New("oups, I did it again")) + Expect(<-reconciled).To(Equal(request)) + Eventually(func() []priorityQueueAddition { + q.lock.Lock() + defer q.lock.Unlock() + return q.added + }).Should(Equal([]priorityQueueAddition{{ + AddOpts: priorityqueue.AddOpts{ + RateLimited: true, + Priority: ptr.To(99), + }, + items: []reconcile.Request{request}, + }})) + }) + PIt("should return if the queue is shutdown", func() { // TODO(community): write this test }) diff --git a/pkg/reconcile/reconcile.go b/pkg/reconcile/reconcile.go index c98b1864ef..1861260161 100644 --- a/pkg/reconcile/reconcile.go +++ b/pkg/reconcile/reconcile.go @@ -44,6 +44,11 @@ type Result struct { // RequeueAfter if greater than 0, tells the Controller to requeue the reconcile key after the Duration. // Implies that Requeue is true, there is no need to set Requeue to true at the same time as RequeueAfter. RequeueAfter time.Duration + + // Priority is the priority that will be used if the item gets re-enqueued (also if an error is returned). + // If Priority is not set the original Priority of the request is preserved. + // Note: Priority is only respected if the controller is using a priorityqueue.PriorityQueue. + Priority *int } // IsZero returns true if this result is empty. From 5e1233d225727fe0136506221fc385af6d142ede Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Sun, 5 Oct 2025 11:40:55 +0200 Subject: [PATCH 17/68] Don't block on Get when queue is shutdown (2nd try) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- pkg/controller/priorityqueue/priorityqueue.go | 15 ++++++++--- .../priorityqueue/priorityqueue_test.go | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue.go b/pkg/controller/priorityqueue/priorityqueue.go index 49942186c0..f702600fc9 100644 --- a/pkg/controller/priorityqueue/priorityqueue.go +++ b/pkg/controller/priorityqueue/priorityqueue.go @@ -290,9 +290,18 @@ func (w *priorityqueue[T]) GetWithPriority() (_ T, priority int, shutdown bool) w.notifyItemOrWaiterAdded() - item := <-w.get - - return item.Key, item.Priority, w.shutdown.Load() + select { + case <-w.done: + // Return if the queue was shutdown while we were already waiting for an item here. + // For example controller workers are continuously calling GetWithPriority and + // GetWithPriority is blocking the workers if there are no items in the queue. + // If the controller and accordingly the queue is then shut down, without this code + // branch the controller workers remain blocked here and are unable to shut down. + var zero T + return zero, 0, true + case item := <-w.get: + return item.Key, item.Priority, w.shutdown.Load() + } } func (w *priorityqueue[T]) Get() (item T, shutdown bool) { diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 10e8f7122d..653f770043 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -296,6 +296,32 @@ var _ = Describe("Controllerworkqueue", func() { Expect(isShutDown).To(BeTrue()) }) + It("Get from priority queue should get unblocked when the priority queue is shut down", func() { + q, _ := newQueue() + + getUnblocked := make(chan struct{}) + + go func() { + defer GinkgoRecover() + defer close(getUnblocked) + + item, priority, isShutDown := q.GetWithPriority() + Expect(item).To(Equal("")) + Expect(priority).To(Equal(0)) + Expect(isShutDown).To(BeTrue()) + }() + + // Verify the go routine above is now waiting for an item. + Eventually(q.waiters.Load).Should(Equal(int64(1))) + Consistently(getUnblocked).ShouldNot(BeClosed()) + + // shut down + q.ShutDown() + + // Verify the shutdown unblocked the go routine. + Eventually(getUnblocked).Should(BeClosed()) + }) + It("items are included in Len() and the queueDepth metric once they are ready", func() { q, metrics := newQueue() defer q.ShutDown() From d2c0c8b55aed8a473b23d4a860103e7d096377be Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Fri, 3 Oct 2025 08:21:47 +0200 Subject: [PATCH 18/68] Enable PQ per default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- examples/priorityqueue/main.go | 3 +-- pkg/config/controller.go | 2 +- pkg/controller/controller.go | 6 +++--- pkg/controller/controller_test.go | 9 +++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/priorityqueue/main.go b/examples/priorityqueue/main.go index 8dacdcc9a3..1dc10c2cbe 100644 --- a/examples/priorityqueue/main.go +++ b/examples/priorityqueue/main.go @@ -24,7 +24,6 @@ import ( "go.uber.org/zap/zapcore" corev1 "k8s.io/api/core/v1" - "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/builder" kubeconfig "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/config" @@ -52,7 +51,7 @@ func run() error { // Setup a Manager mgr, err := manager.New(kubeconfig.GetConfigOrDie(), manager.Options{ - Controller: config.Controller{UsePriorityQueue: ptr.To(true)}, + Controller: config.Controller{}, }) if err != nil { return fmt.Errorf("failed to set up controller-manager: %w", err) diff --git a/pkg/config/controller.go b/pkg/config/controller.go index 3dafaef93b..5eea2965f6 100644 --- a/pkg/config/controller.go +++ b/pkg/config/controller.go @@ -79,7 +79,7 @@ type Controller struct { // UsePriorityQueue configures the controllers queue to use the controller-runtime provided // priority queue. // - // Note: This flag is disabled by default until a future version. This feature is currently in beta. + // Note: This flag is enabled by default. // For more details, see: https://github.com/kubernetes-sigs/controller-runtime/issues/2374. UsePriorityQueue *bool diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index afa15aebec..853788d52f 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -91,7 +91,7 @@ type TypedOptions[request comparable] struct { // UsePriorityQueue configures the controllers queue to use the controller-runtime provided // priority queue. // - // Note: This flag is disabled by default until a future version. This feature is currently in beta. + // Note: This flag is enabled by default. // For more details, see: https://github.com/kubernetes-sigs/controller-runtime/issues/2374. UsePriorityQueue *bool @@ -250,7 +250,7 @@ func NewTypedUnmanaged[request comparable](name string, options TypedOptions[req } if options.RateLimiter == nil { - if ptr.Deref(options.UsePriorityQueue, false) { + if ptr.Deref(options.UsePriorityQueue, true) { options.RateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[request](5*time.Millisecond, 1000*time.Second) } else { options.RateLimiter = workqueue.DefaultTypedControllerRateLimiter[request]() @@ -259,7 +259,7 @@ func NewTypedUnmanaged[request comparable](name string, options TypedOptions[req if options.NewQueue == nil { options.NewQueue = func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request] { - if ptr.Deref(options.UsePriorityQueue, false) { + if ptr.Deref(options.UsePriorityQueue, true) { return priorityqueue.New(controllerName, func(o *priorityqueue.Opts[request]) { o.Log = options.Logger.WithValues("controller", controllerName) o.RateLimiter = rateLimiter diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 335e6d830e..06138a476b 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -439,9 +439,9 @@ var _ = Describe("controller.Controller", func() { Expect(ok).To(BeTrue()) }) - It("should configure a priority queue if UsePriorityQueue is set", func() { + It("should configure a priority queue per default", func() { m, err := manager.New(cfg, manager.Options{ - Controller: config.Controller{UsePriorityQueue: ptr.To(true)}, + Controller: config.Controller{}, }) Expect(err).NotTo(HaveOccurred()) @@ -458,12 +458,13 @@ var _ = Describe("controller.Controller", func() { Expect(ok).To(BeTrue()) }) - It("should not configure a priority queue if UsePriorityQueue is not set", func() { + It("should not configure a priority queue if UsePriorityQueue is set to false", func() { m, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) c, err := controller.New("new-controller-17", m, controller.Options{ - Reconciler: rec, + Reconciler: rec, + UsePriorityQueue: ptr.To(false), }) Expect(err).NotTo(HaveOccurred()) From 975716b023ae290b49dc4eb2ca7ef6d0c0db9fe4 Mon Sep 17 00:00:00 2001 From: Moritz <51033452+moritzmoe@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:37:01 +0200 Subject: [PATCH 19/68] =?UTF-8?q?=F0=9F=90=9B=20Fix=20a=20bug=20where=20th?= =?UTF-8?q?e=20priorityqueue=20would=20sometimes=20not=20return=20high-pri?= =?UTF-8?q?ority=20items=20first=20(#3330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: adjust priority queue order and spin Co-authored-by: kstiehl * fix: do not hand out item during metrics ascend Co-authored-by: kstiehl * test: add test case Co-authored-by: kstiehl * rm async from test * rm metricsAscend flag * fix test Co-authored-by: kstiehl * add comments Co-authored-by: kstiehl * Update pkg/controller/priorityqueue/priorityqueue.go Co-authored-by: Alvaro Aleman --------- Co-authored-by: kstiehl Co-authored-by: Alvaro Aleman --- pkg/controller/priorityqueue/priorityqueue.go | 95 ++++++++++++------- .../priorityqueue/priorityqueue_test.go | 29 ++++++ 2 files changed, 92 insertions(+), 32 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue.go b/pkg/controller/priorityqueue/priorityqueue.go index f702600fc9..98df84c56b 100644 --- a/pkg/controller/priorityqueue/priorityqueue.go +++ b/pkg/controller/priorityqueue/priorityqueue.go @@ -1,6 +1,7 @@ package priorityqueue import ( + "math" "sync" "sync/atomic" "time" @@ -206,6 +207,7 @@ func (w *priorityqueue[T]) spin() { blockForever := make(chan time.Time) var nextReady <-chan time.Time nextReady = blockForever + var nextItemReadyAt time.Time for { select { @@ -213,10 +215,10 @@ func (w *priorityqueue[T]) spin() { return case <-w.itemOrWaiterAdded: case <-nextReady: + nextReady = blockForever + nextItemReadyAt = time.Time{} } - nextReady = blockForever - func() { w.lock.Lock() defer w.lock.Unlock() @@ -227,39 +229,67 @@ func (w *priorityqueue[T]) spin() { // manipulating the tree from within Ascend might lead to panics, so // track what we want to delete and do it after we are done ascending. var toDelete []*item[T] - w.queue.Ascend(func(item *item[T]) bool { - if item.ReadyAt != nil { - if readyAt := item.ReadyAt.Sub(w.now()); readyAt > 0 { - nextReady = w.tick(readyAt) - return false + + var key T + + // Items in the queue tree are sorted first by priority and second by readiness, so + // items with a lower priority might be ready further down in the queue. + // We iterate through the priorities high to low until we find a ready item + pivot := item[T]{ + Key: key, + AddedCounter: 0, + Priority: math.MaxInt, + ReadyAt: nil, + } + + for { + pivotChange := false + + w.queue.AscendGreaterOrEqual(&pivot, func(item *item[T]) bool { + // Item is locked, we can not hand it out + if w.locked.Has(item.Key) { + return true } - if !w.becameReady.Has(item.Key) { - w.metrics.add(item.Key, item.Priority) - w.becameReady.Insert(item.Key) + + if item.ReadyAt != nil { + if readyAt := item.ReadyAt.Sub(w.now()); readyAt > 0 { + if nextItemReadyAt.After(*item.ReadyAt) || nextItemReadyAt.IsZero() { + nextReady = w.tick(readyAt) + nextItemReadyAt = *item.ReadyAt + } + + // Adjusting the pivot item moves the ascend to the next lower priority + pivot.Priority = item.Priority - 1 + pivotChange = true + return false + } + if !w.becameReady.Has(item.Key) { + w.metrics.add(item.Key, item.Priority) + w.becameReady.Insert(item.Key) + } } - } - if w.waiters.Load() == 0 { - // Have to keep iterating here to ensure we update metrics - // for further items that became ready and set nextReady. - return true - } + if w.waiters.Load() == 0 { + // Have to keep iterating here to ensure we update metrics + // for further items that became ready and set nextReady. + return true + } - // Item is locked, we can not hand it out - if w.locked.Has(item.Key) { - return true - } + w.metrics.get(item.Key, item.Priority) + w.locked.Insert(item.Key) + w.waiters.Add(-1) + delete(w.items, item.Key) + toDelete = append(toDelete, item) + w.becameReady.Delete(item.Key) + w.get <- *item - w.metrics.get(item.Key, item.Priority) - w.locked.Insert(item.Key) - w.waiters.Add(-1) - delete(w.items, item.Key) - toDelete = append(toDelete, item) - w.becameReady.Delete(item.Key) - w.get <- *item + return true + }) - return true - }) + if !pivotChange { + break + } + } for _, item := range toDelete { w.queue.Delete(item) @@ -387,6 +417,9 @@ func (w *priorityqueue[T]) logState() { } func less[T comparable](a, b *item[T]) bool { + if a.Priority != b.Priority { + return a.Priority > b.Priority + } if a.ReadyAt == nil && b.ReadyAt != nil { return true } @@ -396,9 +429,6 @@ func less[T comparable](a, b *item[T]) bool { if a.ReadyAt != nil && b.ReadyAt != nil && !a.ReadyAt.Equal(*b.ReadyAt) { return a.ReadyAt.Before(*b.ReadyAt) } - if a.Priority != b.Priority { - return a.Priority > b.Priority - } return a.AddedCounter < b.AddedCounter } @@ -426,4 +456,5 @@ type bTree[T any] interface { ReplaceOrInsert(item T) (_ T, _ bool) Delete(item T) (T, bool) Ascend(iterator btree.ItemIteratorG[T]) + AscendGreaterOrEqual(pivot T, iterator btree.ItemIteratorG[T]) } diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 653f770043..e18a6393eb 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -184,6 +184,35 @@ var _ = Describe("Controllerworkqueue", func() { Expect(metrics.retries["test"]).To(Equal(1)) }) + It("returns high priority item that became ready before low priority item", func() { + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() + defer q.ShutDown() + + tickSetup := make(chan any) + originalTick := q.tick + q.tick = func(d time.Duration) <-chan time.Time { + Expect(d).To(Equal(time.Second)) + close(tickSetup) + return originalTick(d) + } + + lowPriority := -100 + highPriority := 0 + q.AddWithOpts(AddOpts{After: 0, Priority: &lowPriority}, "foo") + q.AddWithOpts(AddOpts{After: time.Second, Priority: &highPriority}, "prio") + + Eventually(tickSetup).Should(BeClosed()) + + forwardQueueTimeBy(1 * time.Second) + key, prio, _ := q.GetWithPriority() + + Expect(key).To(Equal("prio")) + Expect(prio).To(Equal(0)) + Expect(metrics.depth["test"]).To(Equal(map[int]int{-100: 1, 0: 0})) + Expect(metrics.adds["test"]).To(Equal(2)) + Expect(metrics.retries["test"]).To(Equal(1)) + }) + It("returns an item to a waiter as soon as it has one", func() { q, metrics := newQueue() defer q.ShutDown() From a9854cd99117b057220ace4e483e88396133de03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:02:55 +0000 Subject: [PATCH 20/68] :seedling: Bump the all-github-actions group with 2 updates Bumps the all-github-actions group with 2 updates: [ossf/scorecard-action](https://github.com/ossf/scorecard-action) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `ossf/scorecard-action` from 2.4.2 to 2.4.3 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/05b42c624433fc40578a4040d5cf5e36ddca8cde...4eaacf0543bb3f2c246792bd56e8cdeffafb205a) Updates `softprops/action-gh-release` from 2.3.3 to 2.3.4 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/6cbd405e2c4e67a21c47fa9e383d020e4e28b836...62c96d0c4e8a889135c1f3a25910db8dbe0e85f7) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-version: 2.4.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-github-actions - dependency-name: softprops/action-gh-release dependency-version: 2.3.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ossf-scorecard.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ossf-scorecard.yaml b/.github/workflows/ossf-scorecard.yaml index 671dbc88bd..24156f49e0 100644 --- a/.github/workflows/ossf-scorecard.yaml +++ b/.github/workflows/ossf-scorecard.yaml @@ -31,7 +31,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # tag=v2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # tag=v2.4.3 with: results_file: results.sarif results_format: sarif diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b05f2828bb..6e3b7734e7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,7 +29,7 @@ jobs: run: | make release - name: Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # tag=v2.3.3 + uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # tag=v2.3.4 with: draft: false files: tools/setup-envtest/out/* From b9bccfd419149d26d14130887a5e5819e4a3b2be Mon Sep 17 00:00:00 2001 From: Filip Cirtog Date: Fri, 10 Oct 2025 22:37:01 +0200 Subject: [PATCH 21/68] =?UTF-8?q?=F0=9F=90=9B=20Allow=20SSA=20after=20norm?= =?UTF-8?q?al=20resource=20creation=20(#3346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: enable apply after normal create * improvements: cache now considers disable protobuf flag for lookup * feedback improvements --- pkg/client/client.go | 3 +- pkg/client/client_rest_resources.go | 26 +++++++++------- pkg/client/client_test.go | 46 +++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 7e38142273..39050de457 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -151,8 +151,7 @@ func newClient(config *rest.Config, options Options) (*client, error) { mapper: options.Mapper, codecs: serializer.NewCodecFactory(options.Scheme), - structuredResourceByType: make(map[schema.GroupVersionKind]*resourceMeta), - unstructuredResourceByType: make(map[schema.GroupVersionKind]*resourceMeta), + resourceByType: make(map[cacheKey]*resourceMeta), } rawMetaClient, err := metadata.NewForConfigAndClient(metadata.ConfigFor(config), options.HTTPClient) diff --git a/pkg/client/client_rest_resources.go b/pkg/client/client_rest_resources.go index acff7a46a4..d75d685cbb 100644 --- a/pkg/client/client_rest_resources.go +++ b/pkg/client/client_rest_resources.go @@ -48,11 +48,15 @@ type clientRestResources struct { // codecs are used to create a REST client for a gvk codecs serializer.CodecFactory - // structuredResourceByType stores structured type metadata - structuredResourceByType map[schema.GroupVersionKind]*resourceMeta - // unstructuredResourceByType stores unstructured type metadata - unstructuredResourceByType map[schema.GroupVersionKind]*resourceMeta - mu sync.RWMutex + // resourceByType stores type metadata + resourceByType map[cacheKey]*resourceMeta + + mu sync.RWMutex +} + +type cacheKey struct { + gvk schema.GroupVersionKind + forceDisableProtoBuf bool } // newResource maps obj to a Kubernetes Resource and constructs a client for that Resource. @@ -117,11 +121,11 @@ func (c *clientRestResources) getResource(obj any) (*resourceMeta, error) { // It's better to do creation work twice than to not let multiple // people make requests at once c.mu.RLock() - resourceByType := c.structuredResourceByType - if isUnstructured { - resourceByType = c.unstructuredResourceByType - } - r, known := resourceByType[gvk] + + cacheKey := cacheKey{gvk: gvk, forceDisableProtoBuf: forceDisableProtoBuf} + + r, known := c.resourceByType[cacheKey] + c.mu.RUnlock() if known { @@ -140,7 +144,7 @@ func (c *clientRestResources) getResource(obj any) (*resourceMeta, error) { if err != nil { return nil, err } - resourceByType[gvk] = r + c.resourceByType[cacheKey] = r return r, err } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 42e14771cc..021fbeb0d8 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -953,6 +953,52 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(cm.Data).To(BeComparableTo(data)) Expect(cm.Data).To(BeComparableTo(obj.Data)) }) + + It("should create a secret without SSA and later create update a secret using SSA", func(ctx SpecContext) { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + data := map[string][]byte{ + "some-key": []byte("some-value"), + } + secretObject := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret-one", + Namespace: "default", + }, + Data: data, + } + + secretApplyConfiguration := corev1applyconfigurations. + Secret("secret-two", "default"). + WithData(data) + + err = cl.Create(ctx, secretObject) + Expect(err).NotTo(HaveOccurred()) + + err = cl.Apply(ctx, secretApplyConfiguration, &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + secret, err := clientset.CoreV1().Secrets(ptr.Deref(secretApplyConfiguration.GetNamespace(), "")).Get(ctx, ptr.Deref(secretApplyConfiguration.GetName(), ""), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(BeComparableTo(data)) + Expect(secret.Data).To(BeComparableTo(secretApplyConfiguration.Data)) + + data = map[string][]byte{ + "some-key": []byte("some-new-value"), + } + secretApplyConfiguration.Data = data + + err = cl.Apply(ctx, secretApplyConfiguration, &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + secret, err = clientset.CoreV1().Secrets(ptr.Deref(secretApplyConfiguration.GetNamespace(), "")).Get(ctx, ptr.Deref(secretApplyConfiguration.GetName(), ""), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(secret.Data).To(BeComparableTo(data)) + Expect(secret.Data).To(BeComparableTo(secretApplyConfiguration.Data)) + }) }) }) From 7f3f5cbf2881cf781ca47c77ebd1d9fae09436b3 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sat, 11 Oct 2025 20:49:49 -0400 Subject: [PATCH 22/68] :seedling: Bump the k8s.io/* deps to v0.35.0-alpha.1 --- .golangci.yml | 2 +- Makefile | 4 +- examples/scratch-env/go.mod | 43 ++++--- examples/scratch-env/go.sum | 88 +++++++-------- go.mod | 67 ++++++----- go.sum | 137 ++++++++++++----------- pkg/client/apiutil/restmapper_wb_test.go | 2 +- pkg/client/fake/client_test.go | 2 +- pkg/client/options_test.go | 2 +- pkg/reconcile/reconcile_test.go | 4 +- tools/setup-envtest/go.mod | 18 +-- tools/setup-envtest/go.sum | 36 +++--- 12 files changed, 202 insertions(+), 203 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 85701c88a8..5f8edd56b4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ version: "2" run: - go: "1.24" + go: "1.25" timeout: 10m allow-parallel-runners: true linters: diff --git a/Makefile b/Makefile index b8e9cfa877..5dded97481 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ SHELL:=/usr/bin/env bash # # Go. # -GO_VERSION ?= 1.24.0 +GO_VERSION ?= 1.25.0 # Use GOPROXY environment variable if set GOPROXY := $(shell go env GOPROXY) @@ -80,7 +80,7 @@ test-tools: ## tests the tools codebase (setup-envtest) ## Binaries ## -------------------------------------- -GO_APIDIFF_VER := v0.8.2 +GO_APIDIFF_VER := v0.8.3 GO_APIDIFF_BIN := go-apidiff GO_APIDIFF := $(abspath $(TOOLS_BIN_DIR)/$(GO_APIDIFF_BIN)-$(GO_APIDIFF_VER)) GO_APIDIFF_PKG := github.com/joelanford/go-apidiff diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod index 546c7c39ee..06b99d7b0d 100644 --- a/examples/scratch-env/go.mod +++ b/examples/scratch-env/go.mod @@ -1,9 +1,9 @@ module sigs.k8s.io/controller-runtime/examples/scratch-env -go 1.24.0 +go 1.25.0 require ( - github.com/spf13/pflag v1.0.6 + github.com/spf13/pflag v1.0.9 go.uber.org/zap v1.27.0 sigs.k8s.io/controller-runtime v0.0.0-00010101000000-000000000000 ) @@ -16,7 +16,7 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -32,36 +32,35 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.9.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.1 // indirect - k8s.io/apiextensions-apiserver v0.34.1 // indirect - k8s.io/apimachinery v0.34.1 // indirect - k8s.io/client-go v0.34.1 // indirect + k8s.io/api v0.35.0-alpha.1 // indirect + k8s.io/apiextensions-apiserver v0.35.0-alpha.1 // indirect + k8s.io/apimachinery v0.35.0-alpha.1 // indirect + k8s.io/client-go v0.35.0-alpha.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum index 012b88f447..a1bac915e3 100644 --- a/examples/scratch-env/go.sum +++ b/examples/scratch-env/go.sum @@ -17,8 +17,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= @@ -81,18 +81,18 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -102,8 +102,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -127,68 +127,68 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 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.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= -k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/api v0.35.0-alpha.1 h1:aL5Q6ZV4MQ2NZMmlnAsV7wj9a30gLhlLnGbx6GUmuBs= +k8s.io/api v0.35.0-alpha.1/go.mod h1:BoZqpN+rs1nX+WI4b+iOCpHIAZT1A5Cx29nfk4Kn4DY= +k8s.io/apiextensions-apiserver v0.35.0-alpha.1 h1:x/nDc4Ic4j9Pjn8trEuRIkbLgVWkSPTNkDWrNGUnCtg= +k8s.io/apiextensions-apiserver v0.35.0-alpha.1/go.mod h1:g00cZRV928nCiZtLlyedrVInFkJJHxzy8QWCyYJslWQ= +k8s.io/apimachinery v0.35.0-alpha.1 h1:FZCO78xXJf7Bb7oLzw5p6nakz/SWaGTi4+IaOl7uAYk= +k8s.io/apimachinery v0.35.0-alpha.1/go.mod h1:1YSL0XujdSTcnuHOR73D16EdW+d49JOdd8TXjCo6Dhc= +k8s.io/client-go v0.35.0-alpha.1 h1:DbQuaoETvFkhWfIckZj3hj1iNnBvEIdiWjSlosmtlX4= +k8s.io/client-go v0.35.0-alpha.1/go.mod h1:CI5Ggq6AukXNEBV2UeBgY4tfrOZfDSa7KuoWwLfHqGA= 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= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= diff --git a/go.mod b/go.mod index 4d998fe2fc..aab7831236 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,31 @@ module sigs.k8s.io/controller-runtime -go 1.24.0 +go 1.25.0 require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/fsnotify/fsnotify v1.9.0 - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 github.com/google/btree v1.1.3 github.com/google/go-cmp v0.7.0 github.com/google/gofuzz v1.2.0 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 - github.com/prometheus/client_golang v1.22.0 - github.com/prometheus/client_model v0.6.1 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 - golang.org/x/mod v0.21.0 - golang.org/x/sync v0.12.0 - golang.org/x/sys v0.31.0 + golang.org/x/mod v0.27.0 + golang.org/x/sync v0.16.0 + golang.org/x/sys v0.35.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.1 - k8s.io/apiextensions-apiserver v0.34.1 - k8s.io/apimachinery v0.34.1 - k8s.io/apiserver v0.34.1 - k8s.io/client-go v0.34.1 + gopkg.in/evanphx/json-patch.v4 v4.13.0 // Using v4 to match upstream + k8s.io/api v0.35.0-alpha.1 + k8s.io/apiextensions-apiserver v0.35.0-alpha.1 + k8s.io/apimachinery v0.35.0-alpha.1 + k8s.io/apiserver v0.35.0-alpha.1 + k8s.io/client-go v0.35.0-alpha.1 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/structured-merge-diff/v6 v6.3.0 @@ -62,42 +62,41 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/spf13/cobra v1.10.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.26.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/grpc v1.72.1 // indirect - google.golang.org/protobuf v1.36.5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/component-base v0.34.1 // indirect - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/component-base v0.35.0-alpha.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index d6278d8a7d..836a05ccae 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 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= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -102,21 +102,22 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0= +github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= +github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -128,30 +129,30 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -171,40 +172,40 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -213,44 +214,44 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 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.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= -k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= -k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= -k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= +k8s.io/api v0.35.0-alpha.1 h1:aL5Q6ZV4MQ2NZMmlnAsV7wj9a30gLhlLnGbx6GUmuBs= +k8s.io/api v0.35.0-alpha.1/go.mod h1:BoZqpN+rs1nX+WI4b+iOCpHIAZT1A5Cx29nfk4Kn4DY= +k8s.io/apiextensions-apiserver v0.35.0-alpha.1 h1:x/nDc4Ic4j9Pjn8trEuRIkbLgVWkSPTNkDWrNGUnCtg= +k8s.io/apiextensions-apiserver v0.35.0-alpha.1/go.mod h1:g00cZRV928nCiZtLlyedrVInFkJJHxzy8QWCyYJslWQ= +k8s.io/apimachinery v0.35.0-alpha.1 h1:FZCO78xXJf7Bb7oLzw5p6nakz/SWaGTi4+IaOl7uAYk= +k8s.io/apimachinery v0.35.0-alpha.1/go.mod h1:1YSL0XujdSTcnuHOR73D16EdW+d49JOdd8TXjCo6Dhc= +k8s.io/apiserver v0.35.0-alpha.1 h1:y30xMnHnusLzP3IU5rn9prng1dBNdWIXWnDbpEKT914= +k8s.io/apiserver v0.35.0-alpha.1/go.mod h1:Xeoi42Em6YeTr+yx3kFByqlCMIP4nbArQBWSblaH7Vs= +k8s.io/client-go v0.35.0-alpha.1 h1:DbQuaoETvFkhWfIckZj3hj1iNnBvEIdiWjSlosmtlX4= +k8s.io/client-go v0.35.0-alpha.1/go.mod h1:CI5Ggq6AukXNEBV2UeBgY4tfrOZfDSa7KuoWwLfHqGA= +k8s.io/component-base v0.35.0-alpha.1 h1:k7wtwWeS+YbH85qfNimsaDOLhnO28wXazq1YTOjnbQI= +k8s.io/component-base v0.35.0-alpha.1/go.mod h1:TczxAPFOtycFi0/MQwZEJAiaGgXb3/XwZib3CgpgA60= 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= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= diff --git a/pkg/client/apiutil/restmapper_wb_test.go b/pkg/client/apiutil/restmapper_wb_test.go index 73c4236724..5c23b2e6a3 100644 --- a/pkg/client/apiutil/restmapper_wb_test.go +++ b/pkg/client/apiutil/restmapper_wb_test.go @@ -192,7 +192,7 @@ func TestLazyRestMapper_fetchGroupVersionResourcesLocked_CacheInvalidation(t *te g := gmg.NewWithT(t) m := &mapper{ mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}), - client: &fakeAggregatedDiscoveryClient{DiscoveryInterface: fake.NewSimpleClientset().Discovery()}, + client: &fakeAggregatedDiscoveryClient{DiscoveryInterface: fake.NewClientset().Discovery()}, apiGroups: tt.cachedAPIGroups, knownGroups: tt.cachedKnownGroups, } diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 36722b4ddc..6c71d680c0 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -1489,7 +1489,7 @@ var _ = Describe("Fake client", func() { }) It("should be able to build with given tracker and get resource", func(ctx SpecContext) { - clientSet := fake.NewSimpleClientset(dep) + clientSet := fake.NewClientset(dep) cl := NewClientBuilder().WithRuntimeObjects(dep2).WithObjectTracker(clientSet.Tracker()).Build() By("Getting a deployment") diff --git a/pkg/client/options_test.go b/pkg/client/options_test.go index 082586bca3..88ef4a1839 100644 --- a/pkg/client/options_test.go +++ b/pkg/client/options_test.go @@ -307,7 +307,7 @@ var _ = Describe("MatchingLabels", func() { r, _ := listOpts.LabelSelector.Requirements() _, err := labels.NewRequirement(r[0].Key(), r[0].Operator(), r[0].Values().List()) Expect(err).To(HaveOccurred()) - expectedErrMsg := `values[0][k]: Invalid value: "axahm2EJ8Phiephe2eixohbee9eGeiyees1thuozi1xoh0GiuH3diewi8iem7Nui": must be no more than 63 characters` + expectedErrMsg := `values[0][k]: Invalid value: "axahm2EJ8Phiephe2eixohbee9eGeiyees1thuozi1xoh0GiuH3diewi8iem7Nui": must be no more than 63 bytes` Expect(err.Error()).To(Equal(expectedErrMsg)) }) diff --git a/pkg/reconcile/reconcile_test.go b/pkg/reconcile/reconcile_test.go index bb5644b87c..27e9eab471 100644 --- a/pkg/reconcile/reconcile_test.go +++ b/pkg/reconcile/reconcile_test.go @@ -103,10 +103,10 @@ var _ = Describe("reconcile", func() { }) It("should allow unwrapping inner error from terminal error", func() { - inner := apierrors.NewGone("") + inner := apierrors.NewResourceExpired("") terminalError := reconcile.TerminalError(inner) - Expect(apierrors.IsGone(terminalError)).To(BeTrue()) + Expect(apierrors.IsResourceExpired(terminalError)).To(BeTrue()) }) It("should handle nil terminal errors properly", func() { diff --git a/tools/setup-envtest/go.mod b/tools/setup-envtest/go.mod index 15c64f8b57..917187b3b0 100644 --- a/tools/setup-envtest/go.mod +++ b/tools/setup-envtest/go.mod @@ -1,16 +1,16 @@ module sigs.k8s.io/controller-runtime/tools/setup-envtest -go 1.24.0 +go 1.25.0 require ( - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 github.com/onsi/ginkgo/v2 v2.22.2 github.com/onsi/gomega v1.36.2 github.com/spf13/afero v1.12.0 - github.com/spf13/pflag v1.0.6 + github.com/spf13/pflag v1.0.9 go.uber.org/zap v1.27.0 - k8s.io/apimachinery v0.34.1 + k8s.io/apimachinery v0.35.0-alpha.1 sigs.k8s.io/yaml v1.6.0 ) @@ -20,10 +20,10 @@ require ( github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect go.uber.org/multierr v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.28.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/setup-envtest/go.sum b/tools/setup-envtest/go.sum index dfc8e7cce2..f5bb7038b3 100644 --- a/tools/setup-envtest/go.sum +++ b/tools/setup-envtest/go.sum @@ -1,7 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -18,10 +18,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= @@ -32,21 +32,21 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.35.0-alpha.1 h1:FZCO78xXJf7Bb7oLzw5p6nakz/SWaGTi4+IaOl7uAYk= +k8s.io/apimachinery v0.35.0-alpha.1/go.mod h1:1YSL0XujdSTcnuHOR73D16EdW+d49JOdd8TXjCo6Dhc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 08ee0dd5076e416f7c6c56f7efb6ef5d4f2aedbe Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Mon, 13 Oct 2025 11:59:34 -0400 Subject: [PATCH 23/68] :seedling: Priorityqueue tests: Use synctest (#3350) * Priorityqueue: Stop using ginkgo for tests that profit form synctest * Rewrite priorityqueue tests in synctest where applicable * Remove unnecessary nested goroutine --- .../priorityqueue/priorityqueue_test.go | 619 ++++++++++-------- 1 file changed, 328 insertions(+), 291 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index e18a6393eb..fb186944ab 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -5,6 +5,7 @@ import ( "math/rand/v2" "sync" "testing" + "testing/synctest" "time" fuzz "github.com/google/gofuzz" @@ -154,143 +155,6 @@ var _ = Describe("Controllerworkqueue", func() { Expect(metrics.adds["test"]).To(Equal(1)) }) - It("returns an item only after after has passed", func() { - q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() - defer q.ShutDown() - - originalTick := q.tick - q.tick = func(d time.Duration) <-chan time.Time { - Expect(d).To(Equal(time.Second)) - return originalTick(d) - } - - retrievedItem := make(chan struct{}) - - go func() { - defer GinkgoRecover() - q.GetWithPriority() - close(retrievedItem) - }() - - q.AddWithOpts(AddOpts{After: time.Second}, "foo") - - Consistently(retrievedItem).ShouldNot(BeClosed()) - - forwardQueueTimeBy(time.Second) - Eventually(retrievedItem).Should(BeClosed()) - - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) - Expect(metrics.adds["test"]).To(Equal(1)) - Expect(metrics.retries["test"]).To(Equal(1)) - }) - - It("returns high priority item that became ready before low priority item", func() { - q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() - defer q.ShutDown() - - tickSetup := make(chan any) - originalTick := q.tick - q.tick = func(d time.Duration) <-chan time.Time { - Expect(d).To(Equal(time.Second)) - close(tickSetup) - return originalTick(d) - } - - lowPriority := -100 - highPriority := 0 - q.AddWithOpts(AddOpts{After: 0, Priority: &lowPriority}, "foo") - q.AddWithOpts(AddOpts{After: time.Second, Priority: &highPriority}, "prio") - - Eventually(tickSetup).Should(BeClosed()) - - forwardQueueTimeBy(1 * time.Second) - key, prio, _ := q.GetWithPriority() - - Expect(key).To(Equal("prio")) - Expect(prio).To(Equal(0)) - Expect(metrics.depth["test"]).To(Equal(map[int]int{-100: 1, 0: 0})) - Expect(metrics.adds["test"]).To(Equal(2)) - Expect(metrics.retries["test"]).To(Equal(1)) - }) - - It("returns an item to a waiter as soon as it has one", func() { - q, metrics := newQueue() - defer q.ShutDown() - - retrieved := make(chan struct{}) - go func() { - defer GinkgoRecover() - item, _, _ := q.GetWithPriority() - Expect(item).To(Equal("foo")) - close(retrieved) - }() - - // We are waiting for the GetWithPriority() call to be blocked - // on retrieving an item. As golang doesn't provide a way to - // check if something is listening on a channel without - // sending them a message, I can't think of a way to do this - // without sleeping. - time.Sleep(time.Second) - q.AddWithOpts(AddOpts{}, "foo") - Eventually(retrieved).Should(BeClosed()) - - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) - Expect(metrics.adds["test"]).To(Equal(1)) - }) - - It("returns multiple items with after in correct order", func() { - q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() - defer q.ShutDown() - - originalTick := q.tick - q.tick = func(d time.Duration) <-chan time.Time { - // What a bunch of bs. Deferring in here causes - // ginkgo to deadlock, presumably because it - // never returns after the defer. Not deferring - // hides the actual assertion result and makes - // it complain that there should be a defer. - // Move the assertion into a goroutine just to - // get around that mess. - done := make(chan struct{}) - go func() { - defer GinkgoRecover() - defer close(done) - - // This is not deterministic and depends on which of - // Add() or Spin() gets the lock first. - Expect(d).To(Or(Equal(200*time.Millisecond), Equal(time.Second))) - }() - <-done - return originalTick(d) - } - - retrievedItem := make(chan struct{}) - retrievedSecondItem := make(chan struct{}) - - go func() { - defer GinkgoRecover() - first, _, _ := q.GetWithPriority() - Expect(first).To(Equal("bar")) - close(retrievedItem) - - second, _, _ := q.GetWithPriority() - Expect(second).To(Equal("foo")) - close(retrievedSecondItem) - }() - - q.AddWithOpts(AddOpts{After: time.Second}, "foo") - q.AddWithOpts(AddOpts{After: 200 * time.Millisecond}, "bar") - - Consistently(retrievedItem).ShouldNot(BeClosed()) - - forwardQueueTimeBy(time.Second) - Eventually(retrievedItem).Should(BeClosed()) - Eventually(retrievedSecondItem).Should(BeClosed()) - - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) - Expect(metrics.adds["test"]).To(Equal(2)) - }) - It("doesn't include non-ready items in Len()", func() { q, metrics := newQueue() defer q.ShutDown() @@ -325,79 +189,6 @@ var _ = Describe("Controllerworkqueue", func() { Expect(isShutDown).To(BeTrue()) }) - It("Get from priority queue should get unblocked when the priority queue is shut down", func() { - q, _ := newQueue() - - getUnblocked := make(chan struct{}) - - go func() { - defer GinkgoRecover() - defer close(getUnblocked) - - item, priority, isShutDown := q.GetWithPriority() - Expect(item).To(Equal("")) - Expect(priority).To(Equal(0)) - Expect(isShutDown).To(BeTrue()) - }() - - // Verify the go routine above is now waiting for an item. - Eventually(q.waiters.Load).Should(Equal(int64(1))) - Consistently(getUnblocked).ShouldNot(BeClosed()) - - // shut down - q.ShutDown() - - // Verify the shutdown unblocked the go routine. - Eventually(getUnblocked).Should(BeClosed()) - }) - - It("items are included in Len() and the queueDepth metric once they are ready", func() { - q, metrics := newQueue() - defer q.ShutDown() - - q.AddWithOpts(AddOpts{After: 500 * time.Millisecond}, "foo") - q.AddWithOpts(AddOpts{}, "baz") - q.AddWithOpts(AddOpts{After: 500 * time.Millisecond}, "bar") - q.AddWithOpts(AddOpts{}, "bal") - - Expect(q.Len()).To(Equal(2)) - metrics.mu.Lock() - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 2})) - metrics.mu.Unlock() - time.Sleep(time.Second) - Expect(q.Len()).To(Equal(4)) - metrics.mu.Lock() - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 4})) - metrics.mu.Unlock() - - // Drain queue - for range 4 { - item, _ := q.Get() - q.Done(item) - } - Expect(q.Len()).To(Equal(0)) - metrics.mu.Lock() - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) - metrics.mu.Unlock() - - // Validate that doing it again still works to notice bugs with removing - // it from the queues becameReady tracking. - q.AddWithOpts(AddOpts{After: 500 * time.Millisecond}, "foo") - q.AddWithOpts(AddOpts{}, "baz") - q.AddWithOpts(AddOpts{After: 500 * time.Millisecond}, "bar") - q.AddWithOpts(AddOpts{}, "bal") - - Expect(q.Len()).To(Equal(2)) - metrics.mu.Lock() - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 2})) - metrics.mu.Unlock() - time.Sleep(time.Second) - Expect(q.Len()).To(Equal(4)) - metrics.mu.Lock() - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 4})) - metrics.mu.Unlock() - }) - It("returns many items", func() { // This test ensures the queue is able to drain a large queue without panic'ing. // In a previous version of the code we were calling queue.Delete within q.Ascend @@ -460,87 +251,6 @@ var _ = Describe("Controllerworkqueue", func() { Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) metrics.mu.Unlock() }) - - It("Updates metrics correctly for an item whose requeueAfter expired that gets added again without requeueAfter", func() { - q, metrics := newQueue() - defer q.ShutDown() - - q.AddWithOpts(AddOpts{After: 50 * time.Millisecond}, "foo") - time.Sleep(100 * time.Millisecond) - - Expect(q.Len()).To(Equal(1)) - metrics.mu.Lock() - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 1})) - metrics.mu.Unlock() - - q.AddWithOpts(AddOpts{}, "foo") - Expect(q.Len()).To(Equal(1)) - metrics.mu.Lock() - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 1})) - metrics.mu.Unlock() - - // Get the item to ensure the codepath in - // `spin` for the metrics is passed by so - // that this starts failing if it incorrectly - // calls `metrics.add` again. - item, _ := q.Get() - Expect(item).To(Equal("foo")) - Expect(q.Len()).To(Equal(0)) - metrics.mu.Lock() - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) - metrics.mu.Unlock() - }) - - It("When adding items with rateLimit, previous items' rateLimit should not affect subsequent items", func() { - q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() - defer q.ShutDown() - - q.rateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[string](5*time.Millisecond, 1000*time.Second) - originalTick := q.tick - q.tick = func(d time.Duration) <-chan time.Time { - done := make(chan struct{}) - go func() { - defer GinkgoRecover() - defer close(done) - - Expect(d).To(Or(Equal(5*time.Millisecond), Equal(635*time.Millisecond))) - }() - <-done - return originalTick(d) - } - - retrievedItem := make(chan struct{}) - retrievedSecondItem := make(chan struct{}) - - go func() { - defer GinkgoRecover() - first, _, _ := q.GetWithPriority() - Expect(first).To(Equal("foo")) - close(retrievedItem) - - second, _, _ := q.GetWithPriority() - Expect(second).To(Equal("bar")) - close(retrievedSecondItem) - }() - - // after 7 calls, the next When("bar") call will return 640ms. - for range 7 { - q.rateLimiter.When("bar") - } - q.AddWithOpts(AddOpts{RateLimited: true}, "foo", "bar") - - Consistently(retrievedItem).ShouldNot(BeClosed()) - forwardQueueTimeBy(5 * time.Millisecond) - Eventually(retrievedItem).Should(BeClosed()) - - Consistently(retrievedSecondItem).ShouldNot(BeClosed()) - forwardQueueTimeBy(635 * time.Millisecond) - Eventually(retrievedSecondItem).Should(BeClosed()) - - Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) - Expect(metrics.adds["test"]).To(Equal(2)) - Expect(metrics.retries["test"]).To(Equal(2)) - }) }) func BenchmarkAddGetDone(b *testing.B) { @@ -773,3 +483,330 @@ func (b *btreeInteractionValidator) Delete(item *item[string]) (*item[string], b } return old, existed } + +func TestItemIsOnlyReturnedAfterAfterHasPassed(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() + defer q.ShutDown() + + originalTick := q.tick + q.tick = func(d time.Duration) <-chan time.Time { + g.Expect(d).To(Equal(time.Second)) + return originalTick(d) + } + + retrievedItem := make(chan struct{}) + go func() { + q.GetWithPriority() + close(retrievedItem) + }() + + q.AddWithOpts(AddOpts{After: time.Second}, "foo") + synctest.Wait() + + g.Expect(retrievedItem).ShouldNot(BeClosed()) + + forwardQueueTimeBy(time.Second) + synctest.Wait() + g.Expect(retrievedItem).Should(BeClosed()) + + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) + g.Expect(metrics.adds["test"]).To(Equal(1)) + g.Expect(metrics.retries["test"]).To(Equal(1)) + }) +} + +func TestHighPriorityItemThatBecameReadyIsReturnedBeforeLowPriorityItem(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() + defer q.ShutDown() + + tickSetup := make(chan any) + originalTick := q.tick + q.tick = func(d time.Duration) <-chan time.Time { + g.Expect(d).To(Equal(time.Second)) + close(tickSetup) + return originalTick(d) + } + + lowPriority := -100 + highPriority := 0 + q.AddWithOpts(AddOpts{After: 0, Priority: &lowPriority}, "foo") + q.AddWithOpts(AddOpts{After: time.Second, Priority: &highPriority}, "prio") + synctest.Wait() + + g.Expect(tickSetup).To(BeClosed()) + + forwardQueueTimeBy(1 * time.Second) + key, prio, _ := q.GetWithPriority() + + g.Expect(key).To(Equal("prio")) + g.Expect(prio).To(Equal(0)) + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{-100: 1, 0: 0})) + g.Expect(metrics.adds["test"]).To(Equal(2)) + g.Expect(metrics.retries["test"]).To(Equal(1)) + }) +} + +func TestItemIsReturnedAsSoonAsPossible(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics := newQueue() + defer q.ShutDown() + + retrieved := make(chan struct{}) + go func() { + item, _, _ := q.GetWithPriority() + g.Expect(item).To(Equal("foo")) + close(retrieved) + }() + synctest.Wait() // Wait for the above goroutine to be blocked + + q.AddWithOpts(AddOpts{}, "foo") + synctest.Wait() // Wait until the priorityqueue and the above goroutine finish running + + g.Expect(retrieved).Should(BeClosed()) + + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) + g.Expect(metrics.adds["test"]).To(Equal(1)) + }) +} + +func TestMultipleItemsWithAfterAreReturnedInCorrectOrder(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() + defer q.ShutDown() + + originalTick := q.tick + q.tick = func(d time.Duration) <-chan time.Time { + // This is not deterministic and depends on which of + // Add() or Spin() gets the lock first. + g.Expect(d).To(Or(Equal(200*time.Millisecond), Equal(time.Second))) + return originalTick(d) + } + + retrievedItem := make(chan struct{}) + retrievedSecondItem := make(chan struct{}) + + go func() { + first, _, _ := q.GetWithPriority() + g.Expect(first).To(Equal("bar")) + close(retrievedItem) + + second, _, _ := q.GetWithPriority() + g.Expect(second).To(Equal("foo")) + close(retrievedSecondItem) + }() + + q.AddWithOpts(AddOpts{After: time.Second}, "foo") + q.AddWithOpts(AddOpts{After: 200 * time.Millisecond}, "bar") + synctest.Wait() // Block until the adds are processed + + g.Expect(retrievedItem).NotTo(BeClosed()) + + forwardQueueTimeBy(time.Second) + synctest.Wait() // Block until the priorityqueue finished processing + + g.Expect(retrievedItem).Should(BeClosed()) + g.Expect(retrievedSecondItem).Should(BeClosed()) + + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) + g.Expect(metrics.adds["test"]).To(Equal(2)) + }) +} + +func TestGetFromPriorityQueueIsUnblockedOnShutdown(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, _ := newQueue() + + getUnblocked := make(chan struct{}) + + go func() { + defer close(getUnblocked) + + item, priority, isShutDown := q.GetWithPriority() + g.Expect(item).To(Equal("")) + g.Expect(priority).To(Equal(0)) + g.Expect(isShutDown).To(BeTrue()) + }() + synctest.Wait() // Wait for the above goroutine to be blocked + + g.Expect(getUnblocked).NotTo(BeClosed()) + + // shut down + q.ShutDown() + synctest.Wait() + + // Verify the shutdown unblocked the go routine. + g.Expect(getUnblocked).To(BeClosed()) + }) +} + +func TestItemsAreInludedInLenAndMetricsOnceTheyAreReady(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() + defer q.ShutDown() + + q.AddWithOpts(AddOpts{After: 500 * time.Millisecond}, "foo") + q.AddWithOpts(AddOpts{}, "baz") + q.AddWithOpts(AddOpts{After: 500 * time.Millisecond}, "bar") + q.AddWithOpts(AddOpts{}, "bal") + // Block here until spin finished, otherwise it is possible it + // checks now() after forwardQueueTimeBy updated it, does then + // not listen on tick and causes the write to tick from forwardQueueTimeBy + // to lock up the test. + synctest.Wait() + + g.Expect(q.Len()).To(Equal(2)) + metrics.mu.Lock() + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 2})) + metrics.mu.Unlock() + + forwardQueueTimeBy(time.Second) + synctest.Wait() + + g.Expect(q.Len()).To(Equal(4)) + metrics.mu.Lock() + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 4})) + metrics.mu.Unlock() + + // Drain queue + for range 4 { + item, _ := q.Get() + q.Done(item) + } + g.Expect(q.Len()).To(Equal(0)) + metrics.mu.Lock() + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) + metrics.mu.Unlock() + + // Validate that doing it again still works to notice bugs with removing + // it from the queues becameReady tracking. + q.AddWithOpts(AddOpts{After: 500 * time.Millisecond}, "foo") + q.AddWithOpts(AddOpts{}, "baz") + q.AddWithOpts(AddOpts{After: 500 * time.Millisecond}, "bar") + q.AddWithOpts(AddOpts{}, "bal") + // Block here until spin finished, otherwise it is possible it + // checks now() after forwardQueueTimeBy updated it, does then + // not listen on tick and causes the write to tick from forwardQueueTimeBy + // to lock up the test. + synctest.Wait() + + g.Expect(q.Len()).To(Equal(2)) + metrics.mu.Lock() + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 2})) + metrics.mu.Unlock() + + forwardQueueTimeBy(time.Second) + synctest.Wait() + + g.Expect(q.Len()).To(Equal(4)) + metrics.mu.Lock() + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 4})) + metrics.mu.Unlock() + }) +} + +func TestMetricsAreUpdatedForItemWhoseRequeueAfterExpiredThatGetsAddedAgainWithoutRequeueAfter(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() + defer q.ShutDown() + + q.AddWithOpts(AddOpts{After: 50 * time.Millisecond}, "foo") + synctest.Wait() + forwardQueueTimeBy(50 * time.Millisecond) + synctest.Wait() + + g.Expect(q.Len()).To(Equal(1)) + metrics.mu.Lock() + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 1})) + metrics.mu.Unlock() + + q.AddWithOpts(AddOpts{}, "foo") + g.Expect(q.Len()).To(Equal(1)) + metrics.mu.Lock() + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 1})) + metrics.mu.Unlock() + + // Get the item to ensure the codepath in + // `spin` for the metrics is passed by so + // that this starts failing if it incorrectly + // calls `metrics.add` again. + item, _ := q.Get() + g.Expect(item).To(Equal("foo")) + g.Expect(q.Len()).To(Equal(0)) + metrics.mu.Lock() + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) + metrics.mu.Unlock() + }) +} + +func TesWhenAddingMultipleItemsWithRatelimitTrueTheyDontAffectEachOther(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() + defer q.ShutDown() + + q.rateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[string](5*time.Millisecond, 1000*time.Second) + originalTick := q.tick + q.tick = func(d time.Duration) <-chan time.Time { + g.Expect(d).To(Or(Equal(5*time.Millisecond), Equal(635*time.Millisecond))) + return originalTick(d) + } + + retrievedItem := make(chan struct{}) + retrievedSecondItem := make(chan struct{}) + + go func() { + first, _, _ := q.GetWithPriority() + g.Expect(first).To(Equal("foo")) + close(retrievedItem) + + second, _, _ := q.GetWithPriority() + g.Expect(second).To(Equal("bar")) + close(retrievedSecondItem) + }() + + // after 7 calls, the next When("bar") call will return 640ms. + for range 7 { + q.rateLimiter.When("bar") + } + q.AddWithOpts(AddOpts{RateLimited: true}, "foo", "bar") + synctest.Wait() // Block until the adds are processed + g.Expect(retrievedItem).NotTo(BeClosed()) + + forwardQueueTimeBy(5 * time.Millisecond) + synctest.Wait() + g.Expect(retrievedItem).NotTo(BeClosed()) + g.Expect(retrievedSecondItem).NotTo(BeClosed()) + + forwardQueueTimeBy(635 * time.Millisecond) + synctest.Wait() + g.Expect(retrievedSecondItem).To(BeClosed()) + + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) + g.Expect(metrics.adds["test"]).To(Equal(2)) + g.Expect(metrics.retries["test"]).To(Equal(2)) + }) +} From 3153d313a86bdae4a1a47b19952bdb584e2a552f Mon Sep 17 00:00:00 2001 From: Troy Connor Date: Mon, 13 Oct 2025 12:05:27 -0400 Subject: [PATCH 24/68] update List in namespaced client Signed-off-by: Troy Connor --- pkg/client/namespaced_client.go | 7 ++++++- pkg/client/namespaced_client_test.go | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pkg/client/namespaced_client.go b/pkg/client/namespaced_client.go index 445e91b98b..ebbbc4fddf 100644 --- a/pkg/client/namespaced_client.go +++ b/pkg/client/namespaced_client.go @@ -221,7 +221,12 @@ func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object, o // List implements client.Client. func (n *namespacedClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error { - if n.namespace != "" { + isNamespaceScoped, err := n.IsObjectNamespaced(obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %w", err) + } + + if isNamespaceScoped && n.namespace != "" { opts = append(opts, InNamespace(n.namespace)) } return n.client.List(ctx, obj, opts...) diff --git a/pkg/client/namespaced_client_test.go b/pkg/client/namespaced_client_test.go index deae881d4a..7060f32383 100644 --- a/pkg/client/namespaced_client_test.go +++ b/pkg/client/namespaced_client_test.go @@ -54,6 +54,8 @@ var _ = Describe("NamespacedClient", func() { err := rbacv1.AddToScheme(sch) Expect(err).ToNot(HaveOccurred()) + err = corev1.AddToScheme(sch) + Expect(err).ToNot(HaveOccurred()) err = appsv1.AddToScheme(sch) Expect(err).ToNot(HaveOccurred()) @@ -147,6 +149,13 @@ var _ = Describe("NamespacedClient", func() { Expect(result.Items[0]).To(BeEquivalentTo(*dep)) }) + It("should successfully List objects when object is not namespaced scoped", func(ctx SpecContext) { + result := &corev1.NodeList{} + opts := &client.ListOptions{} + Expect(getClient().List(ctx, result, opts)).NotTo(HaveOccurred()) + Expect(result.Items).NotTo(BeEmpty()) + }) + It("should List objects from the namespace specified in the client", func(ctx SpecContext) { result := &appsv1.DeploymentList{} opts := client.InNamespace("non-default") From 6b5cfa1538977c46629ff649b206bb671bf3a675 Mon Sep 17 00:00:00 2001 From: Troy Connor Date: Mon, 13 Oct 2025 15:34:16 -0400 Subject: [PATCH 25/68] add namespace for test with namespace_client Signed-off-by: Troy Connor --- pkg/client/namespaced_client_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/client/namespaced_client_test.go b/pkg/client/namespaced_client_test.go index 7060f32383..6e9635474e 100644 --- a/pkg/client/namespaced_client_test.go +++ b/pkg/client/namespaced_client_test.go @@ -44,6 +44,7 @@ import ( var _ = Describe("NamespacedClient", func() { var dep *appsv1.Deployment + var nameSpace *corev1.Namespace var acDep *appsv1applyconfigurations.DeploymentApplyConfiguration var ns = "default" var count uint64 = 0 @@ -83,6 +84,12 @@ var _ = Describe("NamespacedClient", func() { }, }, } + nameSpace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("namespace-%v", count), + Labels: map[string]string{"name": fmt.Sprintf("namespace-%v", count)}, + }, + } acDep = appsv1applyconfigurations.Deployment(dep.Name, ""). WithLabels(dep.Labels). WithSpec(appsv1applyconfigurations.DeploymentSpec(). @@ -134,10 +141,13 @@ var _ = Describe("NamespacedClient", func() { var err error dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) + nameSpace, err = clientset.CoreV1().Namespaces().Create(ctx, nameSpace, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) }) AfterEach(func(ctx SpecContext) { deleteDeployment(ctx, dep, ns) + deleteNamespace(ctx, nameSpace) }) It("should successfully List objects when namespace is not specified with the object", func(ctx SpecContext) { @@ -150,7 +160,7 @@ var _ = Describe("NamespacedClient", func() { }) It("should successfully List objects when object is not namespaced scoped", func(ctx SpecContext) { - result := &corev1.NodeList{} + result := &corev1.NamespaceList{} opts := &client.ListOptions{} Expect(getClient().List(ctx, result, opts)).NotTo(HaveOccurred()) Expect(result.Items).NotTo(BeEmpty()) From 572fad46e9fd97c1874bc288e9606873924f6c2b Mon Sep 17 00:00:00 2001 From: Borja Clemente Date: Tue, 22 Jul 2025 22:22:05 +0200 Subject: [PATCH 26/68] Add support for the new events API - Add the new events API - Deprecate the old evens API - Add unit and integration tests Signed-off-by: Borja Clemente --- pkg/cluster/cluster.go | 28 +++-- pkg/cluster/cluster_test.go | 11 +- pkg/cluster/internal.go | 5 + pkg/internal/recorder/recorder.go | 106 +++++++++++++++--- .../recorder/recorder_integration_test.go | 43 +++++-- pkg/internal/recorder/recorder_test.go | 29 ++++- pkg/leaderelection/leader_election.go | 6 +- pkg/manager/internal.go | 7 +- pkg/manager/manager.go | 30 +++-- pkg/manager/manager_test.go | 81 ++++++++++--- pkg/recorder/example_test.go | 20 +++- pkg/recorder/recorder.go | 7 +- 12 files changed, 297 insertions(+), 76 deletions(-) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 0603f4cde5..ee14638c3f 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -19,13 +19,16 @@ package cluster import ( "context" "errors" + "fmt" "net/http" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" + eventsv1client "k8s.io/client-go/kubernetes/typed/events/v1" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/cache" @@ -33,10 +36,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/apiutil" logf "sigs.k8s.io/controller-runtime/pkg/internal/log" intrec "sigs.k8s.io/controller-runtime/pkg/internal/recorder" + "sigs.k8s.io/controller-runtime/pkg/recorder" ) // Cluster provides various methods to interact with a cluster. type Cluster interface { + recorder.Provider + // GetHTTPClient returns an HTTP client that can be used to talk to the apiserver GetHTTPClient() *http.Client @@ -58,9 +64,6 @@ type Cluster interface { // GetFieldIndexer returns a client.FieldIndexer configured with the client GetFieldIndexer() client.FieldIndexer - // GetEventRecorderFor returns a new EventRecorder for the provided name - GetEventRecorderFor(name string) record.EventRecorder - // GetRESTMapper returns a RESTMapper GetRESTMapper() meta.RESTMapper @@ -160,8 +163,7 @@ func New(config *rest.Config, opts ...Option) (Cluster, error) { } options, err := setOptionsDefaults(options, config) if err != nil { - options.Logger.Error(err, "Failed to set defaults") - return nil, err + return nil, fmt.Errorf("failed setting cluster default options: %w", err) } // Create the mapper provider @@ -281,16 +283,24 @@ func setOptionsDefaults(options Options, config *rest.Config) (Options, error) { options.newRecorderProvider = intrec.NewProvider } + // This is duplicated with pkg/manager, we need it here to provide + // the user with an EventBroadcaster and there for the Leader election + evtCl, err := eventsv1client.NewForConfigAndClient(config, options.HTTPClient) + if err != nil { + return options, err + } + // This is duplicated with pkg/manager, we need it here to provide // the user with an EventBroadcaster and there for the Leader election if options.EventBroadcaster == nil { // defer initialization to avoid leaking by default - options.makeBroadcaster = func() (record.EventBroadcaster, bool) { - return record.NewBroadcaster(), true + options.makeBroadcaster = func() (record.EventBroadcaster, events.EventBroadcaster, bool) { + return record.NewBroadcaster(), events.NewBroadcaster(&events.EventSinkImpl{Interface: evtCl}), true } } else { - options.makeBroadcaster = func() (record.EventBroadcaster, bool) { - return options.EventBroadcaster, false + // keep supporting the options.EventBroadcaster in the old API, but do not introduce it for the new one. + options.makeBroadcaster = func() (record.EventBroadcaster, events.EventBroadcaster, bool) { + return options.EventBroadcaster, events.NewBroadcaster(&events.EventSinkImpl{Interface: evtCl}), false } } diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index c08a742403..c275ff0bf5 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -40,7 +40,6 @@ var _ = Describe("cluster.Cluster", func() { c, err := New(nil) Expect(c).To(BeNil()) Expect(err.Error()).To(ContainSubstring("must specify Config")) - }) It("should return an error if it can't create a RestMapper", func() { @@ -50,7 +49,6 @@ var _ = Describe("cluster.Cluster", func() { }) Expect(c).To(BeNil()) Expect(err).To(Equal(expected)) - }) It("should return an error it can't create a client.Client", func() { @@ -96,7 +94,6 @@ var _ = Describe("cluster.Cluster", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("expected error")) }) - }) Describe("Start", func() { @@ -160,7 +157,13 @@ var _ = Describe("cluster.Cluster", func() { It("should provide a function to get the EventRecorder", func() { c, err := New(cfg) Expect(err).NotTo(HaveOccurred()) - Expect(c.GetEventRecorderFor("test")).NotTo(BeNil()) + Expect(c.GetEventRecorder("test")).NotTo(BeNil()) + }) + + It("should provide a function to get the deprecated EventRecorder", func() { + c, err := New(cfg) + Expect(err).NotTo(HaveOccurred()) + Expect(c.GetEventRecorderFor("test")).NotTo(BeNil()) //nolint:staticcheck }) It("should provide a function to get the APIReader", func() { c, err := New(cfg) diff --git a/pkg/cluster/internal.go b/pkg/cluster/internal.go index 2742764231..755f83b546 100644 --- a/pkg/cluster/internal.go +++ b/pkg/cluster/internal.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/cache" @@ -87,6 +88,10 @@ func (c *cluster) GetEventRecorderFor(name string) record.EventRecorder { return c.recorderProvider.GetEventRecorderFor(name) } +func (c *cluster) GetEventRecorder(name string) events.EventRecorder { + return c.recorderProvider.GetEventRecorder(name) +} + func (c *cluster) GetRESTMapper() meta.RESTMapper { return c.mapper } diff --git a/pkg/internal/recorder/recorder.go b/pkg/internal/recorder/recorder.go index 21f0146ba3..bbc1604835 100644 --- a/pkg/internal/recorder/recorder.go +++ b/pkg/internal/recorder/recorder.go @@ -24,16 +24,19 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" + eventsv1 "k8s.io/api/events/v1" "k8s.io/apimachinery/pkg/runtime" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" ) // EventBroadcasterProducer makes an event broadcaster, returning // whether or not the broadcaster should be stopped with the Provider, // or not (e.g. if it's shared, it shouldn't be stopped with the Provider). -type EventBroadcasterProducer func() (caster record.EventBroadcaster, stopWithProvider bool) +// This producer currently produces both an old API and a new API broadcaster. +type EventBroadcasterProducer func() (deprecatedCaster record.EventBroadcaster, caster events.EventBroadcaster, stopWithProvider bool) // Provider is a recorder.Provider that records events to the k8s API server // and to a logr Logger. @@ -48,9 +51,13 @@ type Provider struct { evtClient corev1client.EventInterface makeBroadcaster EventBroadcasterProducer - broadcasterOnce sync.Once - broadcaster record.EventBroadcaster - stopBroadcaster bool + broadcasterOnce sync.Once + broadcaster events.EventBroadcaster + cancelSinkRecordingFunc context.CancelFunc + stopWatcherFunc func() + // Deprecated: will be removed in a future release. Use the broadcaster above instead. + deprecatedBroadcaster record.EventBroadcaster + stopBroadcaster bool } // NB(directxman12): this manually implements Stop instead of Being a runnable because we need to @@ -71,10 +78,13 @@ func (p *Provider) Stop(shutdownCtx context.Context) { // almost certainly already been started (e.g. by leader election). We // need to invoke this to ensure that we don't inadvertently race with // an invocation of getBroadcaster. - broadcaster := p.getBroadcaster() + deprecatedBroadcaster, broadcaster := p.getBroadcaster() if p.stopBroadcaster { p.lock.Lock() broadcaster.Shutdown() + p.cancelSinkRecordingFunc() + p.stopWatcherFunc() + deprecatedBroadcaster.Shutdown() p.stopped = true p.lock.Unlock() } @@ -89,7 +99,7 @@ func (p *Provider) Stop(shutdownCtx context.Context) { // getBroadcaster ensures that a broadcaster is started for this // provider, and returns it. It's threadsafe. -func (p *Provider) getBroadcaster() record.EventBroadcaster { +func (p *Provider) getBroadcaster() (record.EventBroadcaster, events.EventBroadcaster) { // NB(directxman12): this can technically still leak if something calls // "getBroadcaster" (i.e. Emits an Event) but never calls Start, but if we // create the broadcaster in start, we could race with other things that @@ -97,17 +107,37 @@ func (p *Provider) getBroadcaster() record.EventBroadcaster { // silently swallowing events and more locking, but that seems suboptimal. p.broadcasterOnce.Do(func() { - broadcaster, stop := p.makeBroadcaster() - broadcaster.StartRecordingToSink(&corev1client.EventSinkImpl{Interface: p.evtClient}) - broadcaster.StartEventWatcher( + p.deprecatedBroadcaster, p.broadcaster, p.stopBroadcaster = p.makeBroadcaster() + + // init deprecated broadcaster + p.deprecatedBroadcaster.StartRecordingToSink(&corev1client.EventSinkImpl{Interface: p.evtClient}) + p.deprecatedBroadcaster.StartEventWatcher( func(e *corev1.Event) { p.logger.V(1).Info(e.Message, "type", e.Type, "object", e.InvolvedObject, "reason", e.Reason) }) - p.broadcaster = broadcaster - p.stopBroadcaster = stop + + // init new broadcaster + ctx, cancel := context.WithCancel(context.Background()) + p.cancelSinkRecordingFunc = cancel + if err := p.broadcaster.StartRecordingToSinkWithContext(ctx); err != nil { + p.logger.Error(err, "error starting recording for broadcaster") + return + } + + stopWatcher, err := p.broadcaster.StartEventWatcher(func(event runtime.Object) { + e, isEvt := event.(*eventsv1.Event) + if isEvt { + p.logger.V(1).Info(e.Note, "type", e.Type, "object", e.Related, "action", e.Action, "reason", e.Reason) + } + }) + if err != nil { + p.logger.Error(err, "error starting event watcher for broadcaster") + } + + p.stopWatcherFunc = stopWatcher }) - return p.broadcaster + return p.deprecatedBroadcaster, p.broadcaster } // NewProvider create a new Provider instance. @@ -128,6 +158,15 @@ func NewProvider(config *rest.Config, httpClient *http.Client, scheme *runtime.S // GetEventRecorderFor returns an event recorder that broadcasts to this provider's // broadcaster. All events will be associated with a component of the given name. func (p *Provider) GetEventRecorderFor(name string) record.EventRecorder { + return &deprecatedRecorder{ + prov: p, + name: name, + } +} + +// GetEventRecorder returns an event recorder that broadcasts to this provider's +// broadcaster. All events will be associated with a component of the given name. +func (p *Provider) GetEventRecorder(name string) events.EventRecorder { return &lazyRecorder{ prov: p, name: name, @@ -141,18 +180,47 @@ type lazyRecorder struct { name string recOnce sync.Once - rec record.EventRecorder + rec events.EventRecorder } // ensureRecording ensures that a concrete recorder is populated for this recorder. func (l *lazyRecorder) ensureRecording() { l.recOnce.Do(func() { - broadcaster := l.prov.getBroadcaster() - l.rec = broadcaster.NewRecorder(l.prov.scheme, corev1.EventSource{Component: l.name}) + _, broadcaster := l.prov.getBroadcaster() + l.rec = broadcaster.NewRecorder(l.prov.scheme, l.name) }) } -func (l *lazyRecorder) Event(object runtime.Object, eventtype, reason, message string) { +func (l *lazyRecorder) Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...any) { + l.ensureRecording() + + l.prov.lock.RLock() + if !l.prov.stopped { + l.rec.Eventf(regarding, related, eventtype, reason, action, note, args...) + } + l.prov.lock.RUnlock() +} + +// deprecatedRecorder implements the old events API during the tranisiton and will be removed in a future release. +// +// Deprecated: will be removed in a future release. +type deprecatedRecorder struct { + prov *Provider + name string + + recOnce sync.Once + rec record.EventRecorder +} + +// ensureRecording ensures that a concrete recorder is populated for this recorder. +func (l *deprecatedRecorder) ensureRecording() { + l.recOnce.Do(func() { + deprecatedBroadcaster, _ := l.prov.getBroadcaster() + l.rec = deprecatedBroadcaster.NewRecorder(l.prov.scheme, corev1.EventSource{Component: l.name}) + }) +} + +func (l *deprecatedRecorder) Event(object runtime.Object, eventtype, reason, message string) { l.ensureRecording() l.prov.lock.RLock() @@ -161,7 +229,8 @@ func (l *lazyRecorder) Event(object runtime.Object, eventtype, reason, message s } l.prov.lock.RUnlock() } -func (l *lazyRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { + +func (l *deprecatedRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...any) { l.ensureRecording() l.prov.lock.RLock() @@ -170,7 +239,8 @@ func (l *lazyRecorder) Eventf(object runtime.Object, eventtype, reason, messageF } l.prov.lock.RUnlock() } -func (l *lazyRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) { + +func (l *deprecatedRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...any) { l.ensureRecording() l.prov.lock.RLock() diff --git a/pkg/internal/recorder/recorder_integration_test.go b/pkg/internal/recorder/recorder_integration_test.go index c278fbde79..061070166c 100644 --- a/pkg/internal/recorder/recorder_integration_test.go +++ b/pkg/internal/recorder/recorder_integration_test.go @@ -21,7 +21,9 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + eventsv1 "k8s.io/api/events/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes/scheme" ref "k8s.io/client-go/tools/reference" @@ -36,20 +38,22 @@ import ( ) var _ = Describe("recorder", func() { - Describe("recorder", func() { + Describe("deprecated recorder", func() { It("should publish events", func(ctx SpecContext) { By("Creating the Manager") cm, err := manager.New(cfg, manager.Options{}) Expect(err).NotTo(HaveOccurred()) By("Creating the Controller") - recorder := cm.GetEventRecorderFor("test-recorder") + deprecatedRecorder := cm.GetEventRecorderFor("test-deprecated-recorder") //nolint:staticcheck + recorder := cm.GetEventRecorder("test-recorder") instance, err := controller.New("foo-controller", cm, controller.Options{ Reconciler: reconcile.Func( func(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { dp, err := clientset.AppsV1().Deployments(request.Namespace).Get(ctx, request.Name, metav1.GetOptions{}) Expect(err).NotTo(HaveOccurred()) - recorder.Event(dp, corev1.EventTypeNormal, "test-reason", "test-msg") + deprecatedRecorder.Event(dp, corev1.EventTypeNormal, "deprecated-test-reason", "deprecated-test-msg") + recorder.Eventf(dp, nil, corev1.EventTypeNormal, "test-reason", "test-action", "test-note") return reconcile.Result{}, nil }), }) @@ -66,7 +70,7 @@ var _ = Describe("recorder", func() { }() deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "deployment-name"}, + ObjectMeta: metav1.ObjectMeta{Name: "deprecated-deployment-name"}, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"foo": "bar"}, @@ -89,23 +93,42 @@ var _ = Describe("recorder", func() { deployment, err = clientset.AppsV1().Deployments("default").Create(ctx, deployment, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) - By("Validate event is published as expected") - evtWatcher, err := clientset.CoreV1().Events("default").Watch(ctx, metav1.ListOptions{}) + // watch both deprecated and new events based on the reason + By("Validate deprecated event is published as expected") + deprecatedEvtWatcher, err := clientset.CoreV1().Events("default").Watch(ctx, + metav1.ListOptions{FieldSelector: fields.OneTermEqualSelector("reason", "deprecated-test-reason").String()}) Expect(err).NotTo(HaveOccurred()) - resultEvent := <-evtWatcher.ResultChan() + resultEvent := <-deprecatedEvtWatcher.ResultChan() Expect(resultEvent.Type).To(Equal(watch.Added)) - evt, isEvent := resultEvent.Object.(*corev1.Event) + deprecatedEvt, isEvent := resultEvent.Object.(*corev1.Event) Expect(isEvent).To(BeTrue()) dpRef, err := ref.GetReference(scheme.Scheme, deployment) Expect(err).NotTo(HaveOccurred()) - Expect(evt.InvolvedObject).To(Equal(*dpRef)) + Expect(deprecatedEvt.InvolvedObject).To(Equal(*dpRef)) + Expect(deprecatedEvt.Type).To(Equal(corev1.EventTypeNormal)) + Expect(deprecatedEvt.Reason).To(Equal("deprecated-test-reason")) + Expect(deprecatedEvt.Message).To(Equal("deprecated-test-msg")) + + By("Validate event is published as expected") + evtWatcher, err := clientset.EventsV1().Events("default").Watch(ctx, + metav1.ListOptions{FieldSelector: fields.OneTermEqualSelector("reason", "test-reason").String()}) + Expect(err).NotTo(HaveOccurred()) + + resultEvent = <-evtWatcher.ResultChan() + + Expect(resultEvent.Type).To(Equal(watch.Added)) + evt, isEvent := resultEvent.Object.(*eventsv1.Event) + Expect(isEvent).To(BeTrue()) + + Expect(evt.Regarding).To(Equal(*dpRef)) Expect(evt.Type).To(Equal(corev1.EventTypeNormal)) Expect(evt.Reason).To(Equal("test-reason")) - Expect(evt.Message).To(Equal("test-msg")) + Expect(evt.Action).To(Equal("test-action")) + Expect(evt.Note).To(Equal("test-note")) }) }) }) diff --git a/pkg/internal/recorder/recorder_test.go b/pkg/internal/recorder/recorder_test.go index e226e165a3..e592a1e189 100644 --- a/pkg/internal/recorder/recorder_test.go +++ b/pkg/internal/recorder/recorder_test.go @@ -21,15 +21,16 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" + eventsv1client "k8s.io/client-go/kubernetes/typed/events/v1" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/internal/recorder" ) var _ = Describe("recorder.Provider", func() { - makeBroadcaster := func() (record.EventBroadcaster, bool) { return record.NewBroadcaster(), true } Describe("NewProvider", func() { It("should return a provider instance and a nil error.", func() { - provider, err := recorder.NewProvider(cfg, httpClient, scheme.Scheme, logr.Discard(), makeBroadcaster) + provider, err := recorder.NewProvider(cfg, httpClient, scheme.Scheme, logr.Discard(), makeBroadcaster()) Expect(provider).NotTo(BeNil()) Expect(err).NotTo(HaveOccurred()) }) @@ -38,18 +39,36 @@ var _ = Describe("recorder.Provider", func() { // Invalid the config cfg1 := *cfg cfg1.Host = "invalid host" - _, err := recorder.NewProvider(&cfg1, httpClient, scheme.Scheme, logr.Discard(), makeBroadcaster) + _, err := recorder.NewProvider(&cfg1, httpClient, scheme.Scheme, logr.Discard(), makeBroadcaster()) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to init client")) }) }) + Describe("GetEventRecorderFor", func() { + It("should return a deprecated recorder instance.", func() { + provider, err := recorder.NewProvider(cfg, httpClient, scheme.Scheme, logr.Discard(), makeBroadcaster()) + Expect(err).NotTo(HaveOccurred()) + + recorder := provider.GetEventRecorderFor("test") + Expect(recorder).NotTo(BeNil()) + }) + }) Describe("GetEventRecorder", func() { It("should return a recorder instance.", func() { - provider, err := recorder.NewProvider(cfg, httpClient, scheme.Scheme, logr.Discard(), makeBroadcaster) + provider, err := recorder.NewProvider(cfg, httpClient, scheme.Scheme, logr.Discard(), makeBroadcaster()) Expect(err).NotTo(HaveOccurred()) - recorder := provider.GetEventRecorderFor("test") + recorder := provider.GetEventRecorder("test") Expect(recorder).NotTo(BeNil()) }) }) }) + +func makeBroadcaster() func() (record.EventBroadcaster, events.EventBroadcaster, bool) { + evtCl, err := eventsv1client.NewForConfigAndClient(cfg, httpClient) + Expect(err).NotTo(HaveOccurred()) + + return func() (record.EventBroadcaster, events.EventBroadcaster, bool) { + return record.NewBroadcaster(), events.NewBroadcaster(&events.EventSinkImpl{Interface: evtCl}), true + } +} diff --git a/pkg/leaderelection/leader_election.go b/pkg/leaderelection/leader_election.go index 6c013e7992..63d875b45a 100644 --- a/pkg/leaderelection/leader_election.go +++ b/pkg/leaderelection/leader_election.go @@ -127,8 +127,10 @@ func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, op corev1Client, coordinationClient, resourcelock.ResourceLockConfig{ - Identity: id, - EventRecorder: recorderProvider.GetEventRecorderFor(id), + Identity: id, + // TODO(clebs): Replace with the new events API after leader election is updated upstream. + // REF: https://github.com/kubernetes/kubernetes/issues/82846 + EventRecorder: recorderProvider.GetEventRecorderFor(id), //nolint:staticcheck }, options.LeaderLabels, ) diff --git a/pkg/manager/internal.go b/pkg/manager/internal.go index a2c3e5324d..4362022b8c 100644 --- a/pkg/manager/internal.go +++ b/pkg/manager/internal.go @@ -32,6 +32,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" "k8s.io/client-go/tools/record" @@ -256,7 +257,11 @@ func (cm *controllerManager) GetCache() cache.Cache { } func (cm *controllerManager) GetEventRecorderFor(name string) record.EventRecorder { - return cm.cluster.GetEventRecorderFor(name) + return cm.cluster.GetEventRecorderFor(name) //nolint:staticcheck +} + +func (cm *controllerManager) GetEventRecorder(name string) events.EventRecorder { + return cm.cluster.GetEventRecorder(name) } func (cm *controllerManager) GetRESTMapper() meta.RESTMapper { diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index e0e94245e7..74983ddcea 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -29,7 +29,9 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" + eventsv1client "k8s.io/client-go/kubernetes/typed/events/v1" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/leaderelection/resourcelock" "k8s.io/client-go/tools/record" "k8s.io/utils/ptr" @@ -337,7 +339,10 @@ func New(config *rest.Config, options Options) (Manager, error) { return nil, errors.New("must specify Config") } // Set default values for options fields - options = setOptionsDefaults(options) + options, err := setOptionsDefaults(config, options) + if err != nil { + return nil, fmt.Errorf("failed setting manager default options: %w", err) + } cluster, err := cluster.New(config, func(clusterOptions *cluster.Options) { clusterOptions.Scheme = options.Scheme @@ -493,7 +498,7 @@ func defaultBaseContext() context.Context { } // setOptionsDefaults set default values for Options fields. -func setOptionsDefaults(options Options) Options { +func setOptionsDefaults(config *rest.Config, options Options) (Options, error) { // Allow newResourceLock to be mocked if options.newResourceLock == nil { options.newResourceLock = leaderelection.NewResourceLock @@ -507,14 +512,25 @@ func setOptionsDefaults(options Options) Options { // This is duplicated with pkg/cluster, we need it here // for the leader election and there to provide the user with // an EventBroadcaster + httpClient, err := rest.HTTPClientFor(config) + if err != nil { + return options, err + } + + evtCl, err := eventsv1client.NewForConfigAndClient(config, httpClient) + if err != nil { + return options, err + } + if options.EventBroadcaster == nil { // defer initialization to avoid leaking by default - options.makeBroadcaster = func() (record.EventBroadcaster, bool) { - return record.NewBroadcaster(), true + options.makeBroadcaster = func() (record.EventBroadcaster, events.EventBroadcaster, bool) { + return record.NewBroadcaster(), events.NewBroadcaster(&events.EventSinkImpl{Interface: evtCl}), true } } else { - options.makeBroadcaster = func() (record.EventBroadcaster, bool) { - return options.EventBroadcaster, false + // keep supporting the options.EventBroadcaster in the old API, but do not introduce it for the new one. + options.makeBroadcaster = func() (record.EventBroadcaster, events.EventBroadcaster, bool) { + return options.EventBroadcaster, events.NewBroadcaster(&events.EventSinkImpl{Interface: evtCl}), false } } @@ -571,5 +587,5 @@ func setOptionsDefaults(options Options) Options { options.WebhookServer = webhook.NewServer(webhook.Options{}) } - return options + return options, nil } diff --git a/pkg/manager/manager_test.go b/pkg/manager/manager_test.go index 4363d62f59..4bf553572d 100644 --- a/pkg/manager/manager_test.go +++ b/pkg/manager/manager_test.go @@ -36,7 +36,10 @@ import ( "go.uber.org/goleak" coordinationv1 "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" + eventsv1 "k8s.io/api/events/v1" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" @@ -60,7 +63,6 @@ var _ = Describe("manger.Manager", func() { m, err := New(nil, Options{}) Expect(m).To(BeNil()) Expect(err.Error()).To(ContainSubstring("must specify Config")) - }) It("should return an error if it can't create a RestMapper", func() { @@ -70,7 +72,6 @@ var _ = Describe("manger.Manager", func() { }) Expect(m).To(BeNil()) Expect(err).To(Equal(expected)) - }) It("should return an error it can't create a client.Client", func() { @@ -207,7 +208,6 @@ var _ = Describe("manger.Manager", func() { } // Don't leak routines <-mgrDone - }) It("should disable gracefulShutdown when stopping to lead", func(ctx SpecContext) { m, err := New(cfg, Options{ @@ -443,7 +443,6 @@ var _ = Describe("manger.Manager", func() { Expect(ok).To(BeTrue()) _, isLeaseLock := cm.resourceLock.(*resourcelock.LeaseLock) Expect(isLeaseLock).To(BeTrue()) - }) It("should use the specified ResourceLock", func() { m, err := New(cfg, Options{ @@ -671,7 +670,7 @@ var _ = Describe("manger.Manager", func() { }) Describe("Start", func() { - var startSuite = func(options Options, callbacks ...func(Manager)) { + startSuite := func(options Options, callbacks ...func(Manager)) { It("should Start each Component", func(ctx SpecContext) { m, err := New(cfg, options) Expect(err).NotTo(HaveOccurred()) @@ -1256,7 +1255,6 @@ var _ = Describe("manger.Manager", func() { <-managerStopDone Expect(time.Since(beforeDone)).To(BeNumerically(">=", 1500*time.Millisecond)) }) - } Context("with defaults", func() { @@ -1790,7 +1788,6 @@ var _ = Describe("manger.Manager", func() { err = m.Start(ctx) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("manager already started")) - }) }) @@ -1810,7 +1807,7 @@ var _ = Describe("manger.Manager", func() { Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed()) }) - It("should not leak goroutines if the default event broadcaster is used & events are emitted", func(specCtx SpecContext) { + It("should not leak goroutines if the deprecated event broadcaster is used & events are emitted", func(specCtx SpecContext) { currentGRs := goleak.IgnoreCurrent() m, err := New(cfg, Options{ /* implicit: default setting for EventBroadcaster */ }) @@ -1820,7 +1817,7 @@ var _ = Describe("manger.Manager", func() { ns := corev1.Namespace{} ns.Name = "default" - recorder := m.GetEventRecorderFor("rock-and-roll") + recorder := m.GetEventRecorderFor("rock-and-roll") //nolint:staticcheck Expect(m.Add(RunnableFunc(func(_ context.Context) error { recorder.Event(&ns, "Warning", "BallroomBlitz", "yeah, yeah, yeah-yeah-yeah") return nil @@ -1858,6 +1855,60 @@ var _ = Describe("manger.Manager", func() { Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed()) }) + It("should not leak goroutines if the default event broadcaster is used & events are emitted", func(specCtx SpecContext) { + currentGRs := goleak.IgnoreCurrent() + + m, err := New(cfg, Options{ /* implicit: default setting for EventBroadcaster */ }) + Expect(err).NotTo(HaveOccurred()) + + By("adding a runnable that emits an event") + ns := corev1.Namespace{} + ns.Name = "default" + + recorder := m.GetEventRecorder("rock-and-roll") + Expect(m.Add(RunnableFunc(func(_ context.Context) error { + recorder.Eventf(&ns, nil, "Warning", "BallroomBlitz", "dance action", "yeah, yeah, yeah-yeah-yeah") + return nil + }))).To(Succeed()) + + By("starting the manager & waiting till we've sent our event") + ctx, cancel := context.WithCancel(specCtx) + doneCh := make(chan struct{}) + go func() { + defer GinkgoRecover() + defer close(doneCh) + Expect(m.Start(ctx)).To(Succeed()) + }() + <-m.Elected() + + Eventually(func() *eventsv1.Event { + evts, err := clientset.EventsV1().Events("").List(ctx, + metav1.ListOptions{FieldSelector: fields.OneTermEqualSelector("regarding.name", ns.Name).String()}) + Expect(err).NotTo(HaveOccurred()) + + for i, evt := range evts.Items { + if evt.Reason == "BallroomBlitz" { + return &evts.Items[i] + } + } + return nil + }).ShouldNot(BeNil()) + + // Sleep between broadcasting start and shutdown to prevent a race condition + // that causes goroutines to leak. + // See pkg/internal/recorder/recorder.go:103 for more info. + time.Sleep(3 * time.Second) + + By("making sure there's no extra go routines still running after we stop") + cancel() + <-doneCh + + // force-close keep-alive connections. These'll time anyway (after + // like 30s or so) but force it to speed up the tests. + clientTransport.CloseIdleConnections() + Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed()) + }) + It("should not leak goroutines when a runnable returns error slowly after being signaled to stop", func(specCtx SpecContext) { // This test reproduces the race condition where the manager's Start method // exits due to context cancellation, leaving no one to drain errChan @@ -1872,7 +1923,7 @@ var _ = Describe("manger.Manager", func() { Expect(err).NotTo(HaveOccurred()) // Add the slow runnable that will return an error after some delay - for i := 0; i < 3; i++ { + for range 3 { slowRunnable := RunnableFunc(func(c context.Context) error { <-c.Done() @@ -1938,10 +1989,15 @@ var _ = Describe("manger.Manager", func() { Expect(m.GetFieldIndexer()).To(Equal(mgr.cluster.GetFieldIndexer())) }) + It("should provide a function to get the deprecated EventRecorder", func() { + m, err := New(cfg, Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(m.GetEventRecorderFor("test")).NotTo(BeNil()) //nolint:staticcheck + }) It("should provide a function to get the EventRecorder", func() { m, err := New(cfg, Options{}) Expect(err).NotTo(HaveOccurred()) - Expect(m.GetEventRecorderFor("test")).NotTo(BeNil()) + Expect(m.GetEventRecorder("test")).NotTo(BeNil()) }) It("should provide a function to get the APIReader", func() { m, err := New(cfg, Options{}) @@ -2020,8 +2076,7 @@ var _ = Describe("manger.Manager", func() { }) }) -type runnableError struct { -} +type runnableError struct{} func (runnableError) Error() string { return "not feeling like that" diff --git a/pkg/recorder/example_test.go b/pkg/recorder/example_test.go index 969420d817..47f14ff715 100644 --- a/pkg/recorder/example_test.go +++ b/pkg/recorder/example_test.go @@ -18,31 +18,39 @@ package recorder_test import ( corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" _ "github.com/onsi/ginkgo/v2" "sigs.k8s.io/controller-runtime/pkg/recorder" ) var ( - recorderProvider recorder.Provider - somePod *corev1.Pod // the object you're reconciling, for example + recorderProvider recorder.Provider + somePod *corev1.Pod // the object you're reconciling, for example + someRelatedObject runtime.Object // another object related to the reconciled object and the event. ) func Example_event() { // recorderProvider is a recorder.Provider - recorder := recorderProvider.GetEventRecorderFor("my-controller") + deprecatedRecorder := recorderProvider.GetEventRecorderFor("my-controller") // emit an event with a fixed message - recorder.Event(somePod, corev1.EventTypeWarning, + deprecatedRecorder.Event(somePod, corev1.EventTypeWarning, "WrongTrousers", "It's the wrong trousers, Gromit!") } func Example_eventf() { // recorderProvider is a recorder.Provider - recorder := recorderProvider.GetEventRecorderFor("my-controller") + deprecatedRecorder := recorderProvider.GetEventRecorderFor("my-controller") // emit an event with a variable message mildCheese := "Wensleydale" - recorder.Eventf(somePod, corev1.EventTypeNormal, + deprecatedRecorder.Eventf(somePod, corev1.EventTypeNormal, "DislikesCheese", "Not even %s?", mildCheese) + + recorder := recorderProvider.GetEventRecorder("my-controller") + + // emit an event with a fixed message + recorder.Eventf(somePod, someRelatedObject, corev1.EventTypeWarning, + "WrongTrousers", "getting dressed", "It's the wrong trousers, Gromit!") } diff --git a/pkg/recorder/recorder.go b/pkg/recorder/recorder.go index f093f0a726..b34fecb525 100644 --- a/pkg/recorder/recorder.go +++ b/pkg/recorder/recorder.go @@ -21,11 +21,16 @@ limitations under the License. package recorder import ( + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" ) // Provider knows how to generate new event recorders with given name. type Provider interface { - // NewRecorder returns an EventRecorder with given name. + // GetEventRecorderFor returns an EventRecorder for the old events API. + // + // Deprecated: this uses the old events API and will be removed in a future release. Please use GetEventRecorder instead. GetEventRecorderFor(name string) record.EventRecorder + // GetEventRecorder returns a EventRecorder with given name. + GetEventRecorder(name string) events.EventRecorder } From 649e5bb660dc9e23fa3701c0c20d68ec785329bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:03:18 +0000 Subject: [PATCH 27/68] :seedling: Bump softprops/action-gh-release Bumps the all-github-actions group with 1 update: [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `softprops/action-gh-release` from 2.3.4 to 2.4.1 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/62c96d0c4e8a889135c1f3a25910db8dbe0e85f7...6da8fa9354ddfdc4aeace5fc48d7f679b5214090) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6e3b7734e7..d9b9f394ef 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,7 +29,7 @@ jobs: run: | make release - name: Release - uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # tag=v2.3.4 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # tag=v2.4.1 with: draft: false files: tools/setup-envtest/out/* From bb473baaf31b16cd02a328c6d66bfb27115dc2cd Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Mon, 13 Oct 2025 17:13:35 -0400 Subject: [PATCH 28/68] Deflake should execute the Warmup function when Warmup group is started The test is flaky as visible in both PRs and the periodic. The reason is that it calls Start() which asynchronously starts a runnable and expects said runnable to finish immediately after which may or may not work out depending on how busy the box the test is run on is. Use `synctest` instead, as that allows us to explicitly block until all asynchronous operations that can finish are finished. --- pkg/manager/runnable_group_test.go | 56 +++++++++++++++++------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/pkg/manager/runnable_group_test.go b/pkg/manager/runnable_group_test.go index 6f9b879e0e..e22f2c00d5 100644 --- a/pkg/manager/runnable_group_test.go +++ b/pkg/manager/runnable_group_test.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "sync/atomic" + "testing" + "testing/synctest" "time" . "github.com/onsi/ginkgo/v2" @@ -110,30 +112,6 @@ var _ = Describe("runnables", func() { Expect(r.Others.startQueue).To(BeEmpty()) }) - It("should execute the Warmup function when Warmup group is started", func(ctx SpecContext) { - var warmupExecuted atomic.Bool - - warmupRunnable := newWarmupRunnableFunc( - func(c context.Context) error { - <-c.Done() - return nil - }, - func(c context.Context) error { - warmupExecuted.Store(true) - return nil - }, - ) - - r := newRunnables(defaultBaseContext, errCh) - Expect(r.Add(warmupRunnable)).To(Succeed()) - - // Start the Warmup group - Expect(r.Warmup.Start(ctx)).To(Succeed()) - - // Verify warmup function was called - Expect(warmupExecuted.Load()).To(BeTrue()) - }) - It("should propagate errors from Warmup function to error channel", func(ctx SpecContext) { expectedErr := fmt.Errorf("expected warmup error") @@ -384,3 +362,33 @@ func newLeaderElectionAndWarmupRunnable( func (r leaderElectionAndWarmupRunnable) NeedLeaderElection() bool { return r.needLeaderElection } + +func TestWarmupFunctionIsExecutedWhenWarmupGroupIsStarted(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + var warmupExecuted atomic.Bool + + warmupRunnable := newWarmupRunnableFunc( + func(c context.Context) error { + <-c.Done() + return nil + }, + func(c context.Context) error { + warmupExecuted.Store(true) + return nil + }, + ) + + r := newRunnables(defaultBaseContext, make(chan error)) + g.Expect(r.Add(warmupRunnable)).To(Succeed()) + + // Start the Warmup group + g.Expect(r.Warmup.Start(t.Context())).To(Succeed()) + synctest.Wait() + + // Verify warmup function was called + g.Expect(warmupExecuted.Load()).To(BeTrue()) + r.Warmup.StopAndWait(t.Context()) + }) +} From e132b976f196a9323d3aedf2d16c082d7fe4cdde Mon Sep 17 00:00:00 2001 From: Troy Connor Date: Thu, 16 Oct 2025 17:39:18 -0400 Subject: [PATCH 29/68] update setup-envtest docs Signed-off-by: Troy Connor --- tools/setup-envtest/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/setup-envtest/README.md b/tools/setup-envtest/README.md index a4de6f3eae..9cbf185cec 100644 --- a/tools/setup-envtest/README.md +++ b/tools/setup-envtest/README.md @@ -4,11 +4,14 @@ This is a small tool that manages binaries for envtest. It can be used to download new binaries, list currently installed and available ones, and clean up versions. -To use it, just go-install it with Golang 1.24+ (it's a separate, self-contained -module): +To use it, download the binary from the [release page.](https://github.com/kubernetes-sigs/controller-runtime/releases) + +If you want to install this with Golang, you can install a release by using a release branch instead. + +NOTE: Each release branch may prefer a different version of Golang when installing. ```shell -go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest +go install sigs.k8s.io/controller-runtime/tools/setup-envtest@release-0.22 ``` If you are using Golang 1.23, use the `release-0.20` branch instead: From 8846a37abe6d6c60fb0c648b3ea8f553242f7481 Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Sat, 4 Oct 2025 16:12:00 +0200 Subject: [PATCH 30/68] Allow implementation of conversion outside of API packages --- pkg/builder/webhook.go | 39 +- pkg/manager/internal.go | 8 + .../internal/integration/manager_test.go | 2 +- pkg/manager/manager.go | 6 + pkg/webhook/conversion/conversion.go | 17 +- pkg/webhook/conversion/conversion_hubspoke.go | 173 ++++++ pkg/webhook/conversion/conversion_registry.go | 57 ++ pkg/webhook/conversion/conversion_test.go | 568 +++++++++++------- 8 files changed, 621 insertions(+), 249 deletions(-) create mode 100644 pkg/webhook/conversion/conversion_hubspoke.go create mode 100644 pkg/webhook/conversion/conversion_registry.go diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go index 6f4726d274..bb5b6deb56 100644 --- a/pkg/builder/webhook.go +++ b/pkg/builder/webhook.go @@ -45,6 +45,7 @@ type WebhookBuilder struct { customPath string customValidatorCustomPath string customDefaulterCustomPath string + converterConstructor func(*runtime.Scheme) (conversion.Converter, error) gvk schema.GroupVersionKind mgr manager.Manager config *rest.Config @@ -86,6 +87,13 @@ func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator) return blder } +// WithConverter takes a func that constructs a converter.Converter. +// The Converter will then be used by the conversion endpoint for the type passed into For(). +func (blder *WebhookBuilder) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder { + blder.converterConstructor = converterConstructor + return blder +} + // WithLogConstructor overrides the webhook's LogConstructor. func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder { blder.logConstructor = logConstructor @@ -287,17 +295,30 @@ func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook { } func (blder *WebhookBuilder) registerConversionWebhook() error { - ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType) - if err != nil { - log.Error(err, "conversion check failed", "GVK", blder.gvk) - return err - } - if ok { - if !blder.isAlreadyHandled("/convert") { - blder.mgr.GetWebhookServer().Register("/convert", conversion.NewWebhookHandler(blder.mgr.GetScheme())) + if blder.converterConstructor != nil { + converter, err := blder.converterConstructor(blder.mgr.GetScheme()) + if err != nil { + return err } - log.Info("Conversion webhook enabled", "GVK", blder.gvk) + + if err := blder.mgr.GetConverterRegistry().RegisterConverter(blder.gvk.GroupKind(), converter); err != nil { + return err + } + } else { + ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType) + if err != nil { + log.Error(err, "conversion check failed", "GVK", blder.gvk) + return err + } + if !ok { + return nil + } + } + + if !blder.isAlreadyHandled("/convert") { + blder.mgr.GetWebhookServer().Register("/convert", conversion.NewWebhookHandler(blder.mgr.GetScheme(), blder.mgr.GetConverterRegistry())) } + log.Info("Conversion webhook enabled", "GVK", blder.gvk) return nil } diff --git a/pkg/manager/internal.go b/pkg/manager/internal.go index 4362022b8c..187d4f56c2 100644 --- a/pkg/manager/internal.go +++ b/pkg/manager/internal.go @@ -36,6 +36,7 @@ import ( "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -130,6 +131,9 @@ type controllerManager struct { // webhookServer if unset, and Add() it to controllerManager. webhookServerOnce sync.Once + // converterRegistry stores conversion.Converter for the conversion endpoint. + converterRegistry conversion.Registry + // leaderElectionID is the name of the resource that leader election // will use for holding the leader lock. leaderElectionID string @@ -284,6 +288,10 @@ func (cm *controllerManager) GetWebhookServer() webhook.Server { return cm.webhookServer } +func (cm *controllerManager) GetConverterRegistry() conversion.Registry { + return cm.converterRegistry +} + func (cm *controllerManager) GetLogger() logr.Logger { return cm.logger } diff --git a/pkg/manager/internal/integration/manager_test.go b/pkg/manager/internal/integration/manager_test.go index c83eead3c1..570c932abc 100644 --- a/pkg/manager/internal/integration/manager_test.go +++ b/pkg/manager/internal/integration/manager_test.go @@ -262,7 +262,7 @@ type ConversionWebhook struct { } func createConversionWebhook(mgr manager.Manager) *ConversionWebhook { - conversionHandler := conversion.NewWebhookHandler(mgr.GetScheme()) + conversionHandler := conversion.NewWebhookHandler(mgr.GetScheme(), mgr.GetConverterRegistry()) httpClient := http.Client{ // Setting a timeout to not get stuck when calling the readiness probe. Timeout: 5 * time.Second, diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index 74983ddcea..af532ea741 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -36,6 +36,7 @@ import ( "k8s.io/client-go/tools/record" "k8s.io/utils/ptr" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -97,6 +98,10 @@ type Manager interface { // GetControllerOptions returns controller global configuration options. GetControllerOptions() config.Controller + + // GetConverterRegistry returns the converter registry that is used to store conversion.Converter + // for the conversion endpoint. + GetConverterRegistry() conversion.Registry } // Options are the arguments for creating a new Manager. @@ -450,6 +455,7 @@ func New(config *rest.Config, options Options) (Manager, error) { logger: options.Logger, elected: make(chan struct{}), webhookServer: options.WebhookServer, + converterRegistry: conversion.NewRegistry(), leaderElectionID: options.LeaderElectionID, leaseDuration: *options.LeaseDuration, renewDeadline: *options.RenewDeadline, diff --git a/pkg/webhook/conversion/conversion.go b/pkg/webhook/conversion/conversion.go index a26fa348bb..3f98fb7ba7 100644 --- a/pkg/webhook/conversion/conversion.go +++ b/pkg/webhook/conversion/conversion.go @@ -43,14 +43,15 @@ var ( log = logf.Log.WithName("conversion-webhook") ) -func NewWebhookHandler(scheme *runtime.Scheme) http.Handler { - return &webhook{scheme: scheme, decoder: NewDecoder(scheme)} +func NewWebhookHandler(scheme *runtime.Scheme, registry Registry) http.Handler { + return &webhook{scheme: scheme, decoder: NewDecoder(scheme), registry: registry} } // webhook implements a CRD conversion webhook HTTP handler. type webhook struct { - scheme *runtime.Scheme - decoder *Decoder + scheme *runtime.Scheme + decoder *Decoder + registry Registry } // ensure Webhook implements http.Handler @@ -119,7 +120,7 @@ func (wh *webhook) handleConvertRequest(ctx context.Context, req *apix.Conversio if err != nil { return nil, err } - err = wh.convertObject(src, dst) + err = wh.convertObject(ctx, src, dst) if err != nil { return nil, err } @@ -137,7 +138,7 @@ func (wh *webhook) handleConvertRequest(ctx context.Context, req *apix.Conversio // convertObject will convert given a src object to dst object. // Note(droot): couldn't find a way to reduce the cyclomatic complexity under 10 // without compromising readability, so disabling gocyclo linter -func (wh *webhook) convertObject(src, dst runtime.Object) error { +func (wh *webhook) convertObject(ctx context.Context, src, dst runtime.Object) error { srcGVK := src.GetObjectKind().GroupVersionKind() dstGVK := dst.GetObjectKind().GroupVersionKind() @@ -149,6 +150,10 @@ func (wh *webhook) convertObject(src, dst runtime.Object) error { return fmt.Errorf("conversion is not allowed between same type %T", src) } + if converter, ok := wh.registry.GetConverter(srcGVK.GroupKind()); ok { + return converter.ConvertObject(ctx, src, dst) + } + srcIsHub, dstIsHub := isHub(src), isHub(dst) srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertible(dst) diff --git a/pkg/webhook/conversion/conversion_hubspoke.go b/pkg/webhook/conversion/conversion_hubspoke.go new file mode 100644 index 0000000000..b33af92ff4 --- /dev/null +++ b/pkg/webhook/conversion/conversion_hubspoke.go @@ -0,0 +1,173 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conversion + +import ( + "context" + "fmt" + "slices" + "strings" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" +) + +func NewHubSpokeConverter[hubObject runtime.Object](hub hubObject, spokeConverter ...SpokeConverter[hubObject]) func(scheme *runtime.Scheme) (Converter, error) { + return func(scheme *runtime.Scheme) (Converter, error) { + hubGVK, err := apiutil.GVKForObject(hub, scheme) + if err != nil { + return nil, fmt.Errorf("failed to create hub spoke converter: failed to get GroupVersionKind for hub: %w", err) + } + allGVKs, err := objectGVKs(scheme, hub) + if err != nil { + return nil, fmt.Errorf("failed to create hub spoke converter for %s: %w", hubGVK.Kind, err) + } + spokeVersions := sets.New[string]() + for _, gvk := range allGVKs { + if gvk != hubGVK { + spokeVersions.Insert(gvk.Version) + } + } + + c := &hubSpokeConverter[hubObject]{ + scheme: scheme, + hubGVK: hubGVK, + spokeConverterByGVK: map[schema.GroupVersionKind]SpokeConverter[hubObject]{}, + } + + spokeConverterVersions := sets.New[string]() + for _, sc := range spokeConverter { + spokeGVK, err := apiutil.GVKForObject(sc.GetSpoke(), scheme) + if err != nil { + return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+ + "failed to get GroupVersionKind for spoke converter: %w", + hubGVK.Kind, err) + } + if hubGVK.GroupKind() != spokeGVK.GroupKind() { + return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+ + "spoke converter GroupKind %s does not match hub GroupKind %s", + hubGVK.Kind, spokeGVK.GroupKind(), hubGVK.GroupKind()) + } + + if _, ok := c.spokeConverterByGVK[spokeGVK]; ok { + return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+ + "duplicate spoke converter for version %s", + hubGVK.Kind, spokeGVK.Version) + } + c.spokeConverterByGVK[spokeGVK] = sc + spokeConverterVersions.Insert(spokeGVK.Version) + } + + if !spokeConverterVersions.Equal(spokeVersions) { + return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+ + "expected spoke converter for %s got spoke converter for %s", + hubGVK.Kind, sortAndJoin(spokeVersions), sortAndJoin(spokeConverterVersions)) + } + + return c, nil + } +} + +func sortAndJoin(set sets.Set[string]) string { + list := set.UnsortedList() + slices.Sort(list) + return strings.Join(list, ",") +} + +type hubSpokeConverter[hubObject runtime.Object] struct { + scheme *runtime.Scheme + hubGVK schema.GroupVersionKind + spokeConverterByGVK map[schema.GroupVersionKind]SpokeConverter[hubObject] +} + +func (c hubSpokeConverter[hubObject]) ConvertObject(ctx context.Context, src, dst runtime.Object) error { + srcGVK := src.GetObjectKind().GroupVersionKind() + dstGVK := dst.GetObjectKind().GroupVersionKind() + + if srcGVK.GroupKind() != dstGVK.GroupKind() { + return fmt.Errorf("src %T and dst %T does not belong to same API Group", src, dst) + } + + if srcGVK == dstGVK { + return fmt.Errorf("conversion is not allowed between same type %T", src) + } + + srcIsHub := c.hubGVK == srcGVK + dstIsHub := c.hubGVK == dstGVK + _, srcIsConvertible := c.spokeConverterByGVK[srcGVK] + _, dstIsConvertible := c.spokeConverterByGVK[dstGVK] + + switch { + case srcIsHub && dstIsConvertible: + return c.spokeConverterByGVK[dstGVK].ConvertHubToSpoke(ctx, src.(hubObject), dst) + case dstIsHub && srcIsConvertible: + return c.spokeConverterByGVK[srcGVK].ConvertSpokeToHub(ctx, src, dst.(hubObject)) + case srcIsConvertible && dstIsConvertible: + hub, err := c.scheme.New(c.hubGVK) + if err != nil { + return fmt.Errorf("failed to allocate an instance for GroupVersionKind %s: %w", c.hubGVK, err) + } + if err := c.spokeConverterByGVK[srcGVK].ConvertSpokeToHub(ctx, src, hub.(hubObject)); err != nil { + return fmt.Errorf("failed to convert spoke %s to hub %s : %w", srcGVK, c.hubGVK, err) + } + if err := c.spokeConverterByGVK[dstGVK].ConvertHubToSpoke(ctx, hub.(hubObject), dst); err != nil { + return fmt.Errorf("failed to convert hub %s to spoke %s : %w", c.hubGVK, dstGVK, err) + } + return nil + default: + return fmt.Errorf("failed to convert %s to %s: not convertible", srcGVK, dstGVK) + } +} + +type SpokeConverter[hubObject runtime.Object] interface { + GetSpoke() runtime.Object + ConvertHubToSpoke(ctx context.Context, hub hubObject, spoke runtime.Object) error + ConvertSpokeToHub(ctx context.Context, spoke runtime.Object, hub hubObject) error +} + +func NewSpokeConverter[hubObject, spokeObject client.Object]( + spoke spokeObject, + convertHubToSpokeFunc func(ctx context.Context, src hubObject, dst spokeObject) error, + convertSpokeToHubFunc func(ctx context.Context, src spokeObject, dst hubObject) error, +) SpokeConverter[hubObject] { + return &spokeConverter[hubObject, spokeObject]{ + spoke: spoke, + convertSpokeToHubFunc: convertSpokeToHubFunc, + convertHubToSpokeFunc: convertHubToSpokeFunc, + } +} + +type spokeConverter[hubObject, spokeObject runtime.Object] struct { + spoke spokeObject + convertHubToSpokeFunc func(ctx context.Context, src hubObject, dst spokeObject) error + convertSpokeToHubFunc func(ctx context.Context, src spokeObject, dst hubObject) error +} + +func (c spokeConverter[hubObject, spokeObject]) GetSpoke() runtime.Object { + return c.spoke +} + +func (c spokeConverter[hubObject, spokeObject]) ConvertHubToSpoke(ctx context.Context, hub hubObject, spoke runtime.Object) error { + return c.convertHubToSpokeFunc(ctx, hub, spoke.(spokeObject)) +} + +func (c spokeConverter[hubObject, spokeObject]) ConvertSpokeToHub(ctx context.Context, spoke runtime.Object, hub hubObject) error { + return c.convertSpokeToHubFunc(ctx, spoke.(spokeObject), hub) +} diff --git a/pkg/webhook/conversion/conversion_registry.go b/pkg/webhook/conversion/conversion_registry.go new file mode 100644 index 0000000000..6e68b5ffa6 --- /dev/null +++ b/pkg/webhook/conversion/conversion_registry.go @@ -0,0 +1,57 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conversion + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type Converter interface { + ConvertObject(ctx context.Context, src, dst runtime.Object) error +} + +type Registry interface { + RegisterConverter(gk schema.GroupKind, converter Converter) error + GetConverter(gk schema.GroupKind) (Converter, bool) +} + +type registry struct { + converterByGK map[schema.GroupKind]Converter +} + +func NewRegistry() Registry { + return registry{ + converterByGK: map[schema.GroupKind]Converter{}, + } +} +func (r registry) RegisterConverter(gk schema.GroupKind, converter Converter) error { + if _, ok := r.converterByGK[gk]; ok { + return fmt.Errorf("failed to register Converter for GroupKind %s: converter already registered", gk) + } + + r.converterByGK[gk] = converter + return nil +} + +func (r registry) GetConverter(gk schema.GroupKind) (Converter, bool) { + c, ok := r.converterByGK[gk] + return c, ok +} diff --git a/pkg/webhook/conversion/conversion_test.go b/pkg/webhook/conversion/conversion_test.go index 489689bccb..046ab44ced 100644 --- a/pkg/webhook/conversion/conversion_test.go +++ b/pkg/webhook/conversion/conversion_test.go @@ -18,6 +18,7 @@ package conversion_test import ( "bytes" + "context" "encoding/json" "io" "net/http" @@ -25,7 +26,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" apix "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -39,285 +40,370 @@ import ( jobsv3 "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/testdata/api/v3" ) -var _ = Describe("Conversion Webhook", func() { - - var respRecorder *httptest.ResponseRecorder - var decoder *conversion.Decoder - var scheme *runtime.Scheme - var wh http.Handler - - BeforeEach(func() { - respRecorder = &httptest.ResponseRecorder{ - Body: bytes.NewBuffer(nil), - } - - scheme = runtime.NewScheme() - Expect(kscheme.AddToScheme(scheme)).To(Succeed()) - Expect(jobsv1.AddToScheme(scheme)).To(Succeed()) - Expect(jobsv2.AddToScheme(scheme)).To(Succeed()) - Expect(jobsv3.AddToScheme(scheme)).To(Succeed()) - - decoder = conversion.NewDecoder(scheme) - wh = conversion.NewWebhookHandler(scheme) - }) - - doRequest := func(convReq *apix.ConversionReview) *apix.ConversionReview { - var payload bytes.Buffer +var _ = Describe("Conversion with Hub/ConvertTo/ConvertFrom methods", func() { + ConversionTest(false) +}) - Expect(json.NewEncoder(&payload).Encode(convReq)).Should(Succeed()) +var _ = Describe("Conversion with HubSpokeConverter", func() { + ConversionTest(true) +}) - convReview := &apix.ConversionReview{} - req := &http.Request{ - Body: io.NopCloser(bytes.NewReader(payload.Bytes())), +func ConversionTest(withHubSpokeConverter bool) { + Describe("Conversion Webhook", func() { + var respRecorder *httptest.ResponseRecorder + var decoder *conversion.Decoder + var scheme *runtime.Scheme + var wh http.Handler + + BeforeEach(func() { + respRecorder = &httptest.ResponseRecorder{ + Body: bytes.NewBuffer(nil), + } + + scheme = runtime.NewScheme() + Expect(kscheme.AddToScheme(scheme)).To(Succeed()) + Expect(jobsv1.AddToScheme(scheme)).To(Succeed()) + Expect(jobsv2.AddToScheme(scheme)).To(Succeed()) + Expect(jobsv3.AddToScheme(scheme)).To(Succeed()) + + decoder = conversion.NewDecoder(scheme) + registry := conversion.NewRegistry() + + if withHubSpokeConverter { + converter, err := conversion.NewHubSpokeConverter(&jobsv2.ExternalJob{}, + conversion.NewSpokeConverter(&jobsv1.ExternalJob{}, convertHubToV1, convertV1ToHub), + conversion.NewSpokeConverter(&jobsv3.ExternalJob{}, convertHubToV3, convertV3ToHub), + )(scheme) + Expect(err).ToNot(HaveOccurred()) + Expect(registry.RegisterConverter(jobsv2.GroupVersion.WithKind("ExternalJob").GroupKind(), converter)).To(Succeed()) + } + + wh = conversion.NewWebhookHandler(scheme, registry) + }) + + doRequest := func(convReq *apix.ConversionReview) *apix.ConversionReview { + var payload bytes.Buffer + + Expect(json.NewEncoder(&payload).Encode(convReq)).Should(Succeed()) + + convReview := &apix.ConversionReview{} + req := &http.Request{ + Body: io.NopCloser(bytes.NewReader(payload.Bytes())), + } + wh.ServeHTTP(respRecorder, req) + Expect(json.NewDecoder(respRecorder.Result().Body).Decode(convReview)).To(Succeed()) + return convReview } - wh.ServeHTTP(respRecorder, req) - Expect(json.NewDecoder(respRecorder.Result().Body).Decode(convReview)).To(Succeed()) - return convReview - } - makeV1Obj := func() *jobsv1.ExternalJob { - return &jobsv1.ExternalJob{ - TypeMeta: metav1.TypeMeta{ - Kind: "ExternalJob", - APIVersion: "jobs.testprojects.kb.io/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "obj-1", - }, - Spec: jobsv1.ExternalJobSpec{ - RunAt: "every 2 seconds", - }, + makeV1Obj := func() *jobsv1.ExternalJob { + return &jobsv1.ExternalJob{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExternalJob", + APIVersion: "jobs.testprojects.kb.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "obj-1", + }, + Spec: jobsv1.ExternalJobSpec{ + RunAt: "every 2 seconds", + }, + } } - } - makeV2Obj := func() *jobsv2.ExternalJob { - return &jobsv2.ExternalJob{ - TypeMeta: metav1.TypeMeta{ - Kind: "ExternalJob", - APIVersion: "jobs.testprojects.kb.io/v2", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "obj-1", - }, - Spec: jobsv2.ExternalJobSpec{ - ScheduleAt: "every 2 seconds", - }, + makeV2Obj := func() *jobsv2.ExternalJob { + return &jobsv2.ExternalJob{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExternalJob", + APIVersion: "jobs.testprojects.kb.io/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "obj-1", + }, + Spec: jobsv2.ExternalJobSpec{ + ScheduleAt: "every 2 seconds", + }, + } } - } - It("should convert spoke to hub successfully", func() { + It("should convert spoke to hub successfully", func() { - v1Obj := makeV1Obj() + v1Obj := makeV1Obj() - expected := &jobsv2.ExternalJob{ - TypeMeta: metav1.TypeMeta{ - Kind: "ExternalJob", - APIVersion: "jobs.testprojects.kb.io/v2", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "obj-1", - }, - Spec: jobsv2.ExternalJobSpec{ - ScheduleAt: "every 2 seconds", - }, - } - - convReq := &apix.ConversionReview{ - TypeMeta: metav1.TypeMeta{}, - Request: &apix.ConversionRequest{ - DesiredAPIVersion: "jobs.testprojects.kb.io/v2", - Objects: []runtime.RawExtension{ - { - Object: v1Obj, + expected := &jobsv2.ExternalJob{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExternalJob", + APIVersion: "jobs.testprojects.kb.io/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "obj-1", + }, + Spec: jobsv2.ExternalJobSpec{ + ScheduleAt: "every 2 seconds", + }, + } + + convReq := &apix.ConversionReview{ + TypeMeta: metav1.TypeMeta{}, + Request: &apix.ConversionRequest{ + DesiredAPIVersion: "jobs.testprojects.kb.io/v2", + Objects: []runtime.RawExtension{ + { + Object: v1Obj, + }, }, }, - }, - } + } - convReview := doRequest(convReq) + convReview := doRequest(convReq) - Expect(convReview.Response.ConvertedObjects).To(HaveLen(1)) - Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusSuccess)) - got, _, err := decoder.Decode(convReview.Response.ConvertedObjects[0].Raw) - Expect(err).NotTo(HaveOccurred()) - Expect(got).To(Equal(expected)) - }) + Expect(convReview.Response.ConvertedObjects).To(HaveLen(1)) + Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusSuccess)) + got, _, err := decoder.Decode(convReview.Response.ConvertedObjects[0].Raw) + Expect(err).NotTo(HaveOccurred()) + Expect(got).To(Equal(expected)) + }) - It("should convert hub to spoke successfully", func() { + It("should convert hub to spoke successfully", func() { - v2Obj := makeV2Obj() + v2Obj := makeV2Obj() - expected := &jobsv1.ExternalJob{ - TypeMeta: metav1.TypeMeta{ - Kind: "ExternalJob", - APIVersion: "jobs.testprojects.kb.io/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "obj-1", - }, - Spec: jobsv1.ExternalJobSpec{ - RunAt: "every 2 seconds", - }, - } - - convReq := &apix.ConversionReview{ - TypeMeta: metav1.TypeMeta{}, - Request: &apix.ConversionRequest{ - DesiredAPIVersion: "jobs.testprojects.kb.io/v1", - Objects: []runtime.RawExtension{ - { - Object: v2Obj, + expected := &jobsv1.ExternalJob{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExternalJob", + APIVersion: "jobs.testprojects.kb.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "obj-1", + }, + Spec: jobsv1.ExternalJobSpec{ + RunAt: "every 2 seconds", + }, + } + + convReq := &apix.ConversionReview{ + TypeMeta: metav1.TypeMeta{}, + Request: &apix.ConversionRequest{ + DesiredAPIVersion: "jobs.testprojects.kb.io/v1", + Objects: []runtime.RawExtension{ + { + Object: v2Obj, + }, }, }, - }, - } + } - convReview := doRequest(convReq) + convReview := doRequest(convReq) - Expect(convReview.Response.ConvertedObjects).To(HaveLen(1)) - Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusSuccess)) - got, _, err := decoder.Decode(convReview.Response.ConvertedObjects[0].Raw) - Expect(err).NotTo(HaveOccurred()) - Expect(got).To(Equal(expected)) - }) + Expect(convReview.Response.ConvertedObjects).To(HaveLen(1)) + Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusSuccess)) + got, _, err := decoder.Decode(convReview.Response.ConvertedObjects[0].Raw) + Expect(err).NotTo(HaveOccurred()) + Expect(got).To(Equal(expected)) + }) - It("should convert spoke to spoke successfully", func() { + It("should convert spoke to spoke successfully", func() { - v1Obj := makeV1Obj() + v1Obj := makeV1Obj() - expected := &jobsv3.ExternalJob{ - TypeMeta: metav1.TypeMeta{ - Kind: "ExternalJob", - APIVersion: "jobs.testprojects.kb.io/v3", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "obj-1", - }, - Spec: jobsv3.ExternalJobSpec{ - DeferredAt: "every 2 seconds", - }, - } - - convReq := &apix.ConversionReview{ - TypeMeta: metav1.TypeMeta{}, - Request: &apix.ConversionRequest{ - DesiredAPIVersion: "jobs.testprojects.kb.io/v3", - Objects: []runtime.RawExtension{ - { - Object: v1Obj, + expected := &jobsv3.ExternalJob{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExternalJob", + APIVersion: "jobs.testprojects.kb.io/v3", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "obj-1", + }, + Spec: jobsv3.ExternalJobSpec{ + DeferredAt: "every 2 seconds", + }, + } + + convReq := &apix.ConversionReview{ + TypeMeta: metav1.TypeMeta{}, + Request: &apix.ConversionRequest{ + DesiredAPIVersion: "jobs.testprojects.kb.io/v3", + Objects: []runtime.RawExtension{ + { + Object: v1Obj, + }, }, }, - }, - } + } + + convReview := doRequest(convReq) + + Expect(convReview.Response.ConvertedObjects).To(HaveLen(1)) + Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusSuccess)) + got, _, err := decoder.Decode(convReview.Response.ConvertedObjects[0].Raw) + Expect(err).NotTo(HaveOccurred()) + Expect(got).To(Equal(expected)) + }) + + It("should return error when dest/src objects belong to different API groups", func() { + v1Obj := makeV1Obj() + + convReq := &apix.ConversionReview{ + TypeMeta: metav1.TypeMeta{}, + Request: &apix.ConversionRequest{ + // request conversion for different group + DesiredAPIVersion: "jobss.example.org/v2", + Objects: []runtime.RawExtension{ + { + Object: v1Obj, + }, + }, + }, + } - convReview := doRequest(convReq) + convReview := doRequest(convReq) + Expect(convReview.Response.Result.Status).To(Equal("Failure")) + Expect(convReview.Response.ConvertedObjects).To(BeEmpty()) + }) - Expect(convReview.Response.ConvertedObjects).To(HaveLen(1)) - Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusSuccess)) - got, _, err := decoder.Decode(convReview.Response.ConvertedObjects[0].Raw) - Expect(err).NotTo(HaveOccurred()) - Expect(got).To(Equal(expected)) - }) + It("should return error when dest/src objects are of same type", func() { + + v1Obj := makeV1Obj() - It("should return error when dest/src objects belong to different API groups", func() { - v1Obj := makeV1Obj() - - convReq := &apix.ConversionReview{ - TypeMeta: metav1.TypeMeta{}, - Request: &apix.ConversionRequest{ - // request conversion for different group - DesiredAPIVersion: "jobss.example.org/v2", - Objects: []runtime.RawExtension{ - { - Object: v1Obj, + convReq := &apix.ConversionReview{ + TypeMeta: metav1.TypeMeta{}, + Request: &apix.ConversionRequest{ + DesiredAPIVersion: "jobs.testprojects.kb.io/v1", + Objects: []runtime.RawExtension{ + { + Object: v1Obj, + }, }, }, - }, - } - - convReview := doRequest(convReq) - Expect(convReview.Response.Result.Status).To(Equal("Failure")) - Expect(convReview.Response.ConvertedObjects).To(BeEmpty()) - }) + } - It("should return error when dest/src objects are of same type", func() { + convReview := doRequest(convReq) + Expect(convReview.Response.Result.Status).To(Equal("Failure")) + Expect(convReview.Response.ConvertedObjects).To(BeEmpty()) + }) - v1Obj := makeV1Obj() + It("should return error when the API group does not have a hub defined", func() { - convReq := &apix.ConversionReview{ - TypeMeta: metav1.TypeMeta{}, - Request: &apix.ConversionRequest{ - DesiredAPIVersion: "jobs.testprojects.kb.io/v1", - Objects: []runtime.RawExtension{ - { - Object: v1Obj, + v1Obj := &appsv1beta1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "obj-1", + }, + } + + convReq := &apix.ConversionReview{ + TypeMeta: metav1.TypeMeta{}, + Request: &apix.ConversionRequest{ + DesiredAPIVersion: "apps/v1", + Objects: []runtime.RawExtension{ + { + Object: v1Obj, + }, }, }, - }, - } + } + + convReview := doRequest(convReq) + Expect(convReview.Response.Result.Status).To(Equal("Failure")) + Expect(convReview.Response.ConvertedObjects).To(BeEmpty()) + }) + + It("should return error on panic in conversion", func() { + + v1Obj := makeV1Obj() + v1Obj.Spec.PanicInConversion = true + + convReq := &apix.ConversionReview{ + TypeMeta: metav1.TypeMeta{}, + Request: &apix.ConversionRequest{ + DesiredAPIVersion: "jobs.testprojects.kb.io/v3", + Objects: []runtime.RawExtension{ + { + Object: v1Obj, + }, + }, + }, + } - convReview := doRequest(convReq) - Expect(convReview.Response.Result.Status).To(Equal("Failure")) - Expect(convReview.Response.ConvertedObjects).To(BeEmpty()) - }) + convReview := doRequest(convReq) - It("should return error when the API group does not have a hub defined", func() { + Expect(convReview.Response.ConvertedObjects).To(HaveLen(0)) + Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusFailure)) + Expect(convReview.Response.Result.Message).To(Equal("internal error occurred during conversion")) + }) + }) +} - v1Obj := &appsv1beta1.Deployment{ - TypeMeta: metav1.TypeMeta{ - Kind: "Deployment", - APIVersion: "apps/v1beta1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "obj-1", - }, - } +var _ = Describe("NewHubSpokeConverter", func() { + var scheme *runtime.Scheme - convReq := &apix.ConversionReview{ - TypeMeta: metav1.TypeMeta{}, - Request: &apix.ConversionRequest{ - DesiredAPIVersion: "apps/v1", - Objects: []runtime.RawExtension{ - { - Object: v1Obj, - }, - }, - }, - } + BeforeEach(func() { + scheme = runtime.NewScheme() + Expect(jobsv1.AddToScheme(scheme)).To(Succeed()) + Expect(jobsv2.AddToScheme(scheme)).To(Succeed()) + Expect(jobsv3.AddToScheme(scheme)).To(Succeed()) + }) - convReview := doRequest(convReq) - Expect(convReview.Response.Result.Status).To(Equal("Failure")) - Expect(convReview.Response.ConvertedObjects).To(BeEmpty()) + It("should succeed if all converter are specified", func() { + _, err := conversion.NewHubSpokeConverter(&jobsv2.ExternalJob{}, + conversion.NewSpokeConverter(&jobsv1.ExternalJob{}, convertHubToV1, convertV1ToHub), + conversion.NewSpokeConverter(&jobsv3.ExternalJob{}, convertHubToV3, convertV3ToHub), + )(scheme) + Expect(err).ToNot(HaveOccurred()) }) - It("should return error on panic in conversion", func() { + It("should return error if hub is not registered in the scheme", func() { + _, err := conversion.NewHubSpokeConverter(&appsv1.Deployment{})(scheme) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to create hub spoke converter: failed to get GroupVersionKind for hub: no kind is registered for the type v1.Deployment in scheme \"pkg/runtime/scheme.go:111\"")) + }) - v1Obj := makeV1Obj() - v1Obj.Spec.PanicInConversion = true + It("should return error if spoke is not registered in the scheme", func() { + _, err := conversion.NewHubSpokeConverter(&jobsv2.ExternalJob{}, + conversion.NewSpokeConverter(&appsv1.Deployment{}, + func(context.Context, *jobsv2.ExternalJob, *appsv1.Deployment) error { return nil }, + func(context.Context, *appsv1.Deployment, *jobsv2.ExternalJob) error { return nil }), + )(scheme) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to create hub spoke converter for ExternalJob: failed to get GroupVersionKind for spoke converter: no kind is registered for the type v1.Deployment in scheme \"pkg/runtime/scheme.go:111\"")) + }) - convReq := &apix.ConversionReview{ - TypeMeta: metav1.TypeMeta{}, - Request: &apix.ConversionRequest{ - DesiredAPIVersion: "jobs.testprojects.kb.io/v3", - Objects: []runtime.RawExtension{ - { - Object: v1Obj, - }, - }, - }, - } + It("should return error if spoke does not have the same GroupKind as the hub", func() { + _ = kscheme.AddToScheme(scheme) + _, err := conversion.NewHubSpokeConverter(&jobsv2.ExternalJob{}, + conversion.NewSpokeConverter(&appsv1.Deployment{}, + func(context.Context, *jobsv2.ExternalJob, *appsv1.Deployment) error { return nil }, + func(context.Context, *appsv1.Deployment, *jobsv2.ExternalJob) error { return nil }), + )(scheme) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to create hub spoke converter for ExternalJob: spoke converter GroupKind Deployment.apps does not match hub GroupKind ExternalJob.jobs.testprojects.kb.io")) + }) - convReview := doRequest(convReq) + It("should return error if same spoke is specified twice", func() { + _, err := conversion.NewHubSpokeConverter(&jobsv2.ExternalJob{}, + conversion.NewSpokeConverter(&jobsv1.ExternalJob{}, convertHubToV1, convertV1ToHub), + conversion.NewSpokeConverter(&jobsv3.ExternalJob{}, convertHubToV3, convertV3ToHub), // duplicate + conversion.NewSpokeConverter(&jobsv3.ExternalJob{}, convertHubToV3, convertV3ToHub), // duplicate + )(scheme) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to create hub spoke converter for ExternalJob: duplicate spoke converter for version v3")) + }) - Expect(convReview.Response.ConvertedObjects).To(HaveLen(0)) - Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusFailure)) - Expect(convReview.Response.Result.Message).To(Equal("internal error occurred during conversion")) + It("should return error if a converter is missing", func() { + _, err := conversion.NewHubSpokeConverter(&jobsv2.ExternalJob{}, + conversion.NewSpokeConverter(&jobsv1.ExternalJob{}, convertHubToV1, convertV1ToHub), + // jobsv3.ExternalJob converter is missing + )(scheme) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("failed to create hub spoke converter for ExternalJob: expected spoke converter for v1,v3 got spoke converter for v1")) }) }) @@ -381,3 +467,19 @@ var _ = Describe("IsConvertible", func() { Expect(ok).ToNot(BeTrue()) }) }) + +func convertV1ToHub(_ context.Context, src *jobsv1.ExternalJob, dst *jobsv2.ExternalJob) error { + return src.ConvertTo(dst) +} + +func convertHubToV1(_ context.Context, src *jobsv2.ExternalJob, dst *jobsv1.ExternalJob) error { + return dst.ConvertFrom(src) +} + +func convertV3ToHub(_ context.Context, src *jobsv3.ExternalJob, dst *jobsv2.ExternalJob) error { + return src.ConvertTo(dst) +} + +func convertHubToV3(_ context.Context, src *jobsv2.ExternalJob, dst *jobsv3.ExternalJob) error { + return dst.ConvertFrom(src) +} From 9893d5b1d172970b8c33ecbcf247114a434fd7df Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Sat, 25 Oct 2025 06:50:16 +0200 Subject: [PATCH 31/68] fake client: fix SSA after List with non-list kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- pkg/client/client_test.go | 37 ++++++++++++++++++++++++++++++++++ pkg/client/fake/client.go | 6 ++++++ pkg/client/fake/client_test.go | 34 +++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 021fbeb0d8..079458f527 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -2516,6 +2516,43 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(hasDep).To(BeTrue()) }) + It("should fetch unstructured collection of objects when setting a non-list kind", func(ctx SpecContext) { + // While it is not ideal to omit the List suffix it can easily happen. + // As the client is using TrimSuffix(gvk.Kind,"List") this also works. + // As it works it is part of our API and we should test it accordingly. + By("create an initial object") + _, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing all objects of that type in the cluster") + deps := &unstructured.UnstructuredList{} + deps.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + }) + err = cl.List(ctx, deps) + Expect(err).NotTo(HaveOccurred()) + + Expect(deps.Items).NotTo(BeEmpty()) + hasDep := false + for _, item := range deps.Items { + Expect(item.GroupVersionKind()).To(Equal(schema.GroupVersionKind{ + Group: "apps", + Kind: "Deployment", + Version: "v1", + })) + if item.GetName() == dep.Name && item.GetNamespace() == dep.Namespace { + hasDep = true + break + } + } + Expect(hasDep).To(BeTrue()) + }) + It("should fetch unstructured collection of objects, even if scheme is empty", func(ctx SpecContext) { By("create an initial object") _, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{}) diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 62067cb19c..05d71bac76 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -1701,6 +1701,12 @@ func (c *fakeClient) addToSchemeIfUnknownAndUnstructuredOrPartial(obj runtime.Ob return err } + if isUnstructuredList || isPartialList { + if !strings.HasSuffix(gvk.Kind, "List") { + gvk.Kind += "List" + } + } + if !c.scheme.Recognizes(gvk) { c.scheme.AddKnownTypeWithName(gvk, obj) } diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 6c71d680c0..1eed4d5a6d 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -2939,6 +2939,40 @@ var _ = Describe("Fake client", func() { Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) }) + It("supports server-side apply of a custom resource via Apply method after List with a non-list kind", func(ctx SpecContext) { + cl := NewClientBuilder().Build() + + // Previously this List call lead to addToSchemeIfUnknownAndUnstructuredOrPartial adding FakeResource as UnstructuredList to the scheme. + // This broke the subsequent SSA call as it was trying to create a new FakeResource object as an UnstructuredList. + // After a fix this List call leads to addToSchemeIfUnknownAndUnstructuredOrPartial adding FakeResourceList as UnstructuredList to the + // scheme even if the UnstructuredList here is missing the List suffix. + objList := &unstructured.UnstructuredList{} + objList.SetAPIVersion("custom/v1") + objList.SetKind("FakeResource") + Expect(cl.List(ctx, objList)).To(Succeed()) + + obj := &unstructured.Unstructured{} + obj.SetAPIVersion("custom/v1") + obj.SetKind("FakeResource") + obj.SetName("foo") + result := obj.DeepCopy() + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"some": "data"}, "spec")).To(Succeed()) + + applyConfig := client.ApplyConfigurationFromUnstructured(obj) + Expect(cl.Apply(ctx, applyConfig, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) + Expect(result.Object["spec"]).To(Equal(map[string]any{"some": "data"})) + + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "spec")).To(Succeed()) + applyConfig2 := client.ApplyConfigurationFromUnstructured(obj) + Expect(cl.Apply(ctx, applyConfig2, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) + Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) + }) + It("sets the fieldManager in create, patch and update", func(ctx SpecContext) { owner := "test-owner" cl := client.WithFieldOwner( From 2e833dde5f912dc7f77152938f509805e644ec66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 20:28:14 +0000 Subject: [PATCH 32/68] :seedling: Bump actions/upload-artifact in the all-github-actions group Bumps the all-github-actions group with 1 update: [actions/upload-artifact](https://github.com/actions/upload-artifact). Updates `actions/upload-artifact` from 4.6.2 to 5.0.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...330a01c490aca151604b8cf639adc76d48f6c5d4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ossf-scorecard.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ossf-scorecard.yaml b/.github/workflows/ossf-scorecard.yaml index 24156f49e0..379fb88557 100644 --- a/.github/workflows/ossf-scorecard.yaml +++ b/.github/workflows/ossf-scorecard.yaml @@ -43,7 +43,7 @@ jobs: # Upload the results as artifacts. - name: "Upload artifact" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # tag=v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # tag=v5.0.0 with: name: SARIF file path: results.sarif From b043f01aed9134e86ea74f8ff3cf842a47e9565b Mon Sep 17 00:00:00 2001 From: s-z-z Date: Tue, 28 Oct 2025 09:43:12 +0000 Subject: [PATCH 33/68] fix(testing/addr): prevent possible leak by removing defer in loop and update description as `portReserveTime` constant --- pkg/internal/testing/addr/manager.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/internal/testing/addr/manager.go b/pkg/internal/testing/addr/manager.go index ffa33a8861..2e2e41323a 100644 --- a/pkg/internal/testing/addr/manager.go +++ b/pkg/internal/testing/addr/manager.go @@ -124,14 +124,16 @@ func suggest(listenHost string) (*net.TCPListener, int, string, error) { // Suggest suggests an address a process can listen on. It returns // a tuple consisting of a free port and the hostname resolved to its IP. // It makes sure that new port allocated does not conflict with old ports -// allocated within 1 minute. +// allocated within 2 minute. func Suggest(listenHost string) (int, string, error) { for i := 0; i < portConflictRetry; i++ { listener, port, resolvedHost, err := suggest(listenHost) if err != nil { return -1, "", err } - defer listener.Close() + if err := listener.Close(); err != nil { + return -1, "", err + } if ok, err := cache.add(port); ok { return port, resolvedHost, nil } else if err != nil { From bef09079762e1f9e950889787dba1b8c9eecd8f8 Mon Sep 17 00:00:00 2001 From: fossedihelm Date: Tue, 28 Oct 2025 18:45:56 +0100 Subject: [PATCH 34/68] priority queue: properly sync the `waiter` manipulation As described in https://github.com/kubernetes-sigs/controller-runtime/issues/3363, there are some circumstances under which `GetWithPriority` is not returning the correct/expected element. This can happen when a `GetWithPriority` is executed and the `Ascend` of the queue is not completed yet, causing not all the items of the BTree to evaluate the same w.waiters.Load() value. Adding a lock to manipulate the waiters will solve the issue. Since the lock is required, there is no need to use an atomic.Int64 anymore. Signed-off-by: fossedihelm --- pkg/controller/priorityqueue/priorityqueue.go | 12 +++-- .../priorityqueue/priorityqueue_test.go | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue.go b/pkg/controller/priorityqueue/priorityqueue.go index 98df84c56b..71363f0d17 100644 --- a/pkg/controller/priorityqueue/priorityqueue.go +++ b/pkg/controller/priorityqueue/priorityqueue.go @@ -124,8 +124,8 @@ type priorityqueue[T comparable] struct { get chan item[T] // waiters is the number of routines blocked in Get, we use it to determine - // if we can push items. - waiters atomic.Int64 + // if we can push items. Every manipulation has to be protected with the lock. + waiters int64 // Configurable for testing now func() time.Time @@ -269,7 +269,7 @@ func (w *priorityqueue[T]) spin() { } } - if w.waiters.Load() == 0 { + if w.waiters == 0 { // Have to keep iterating here to ensure we update metrics // for further items that became ready and set nextReady. return true @@ -277,7 +277,7 @@ func (w *priorityqueue[T]) spin() { w.metrics.get(item.Key, item.Priority) w.locked.Insert(item.Key) - w.waiters.Add(-1) + w.waiters-- delete(w.items, item.Key) toDelete = append(toDelete, item) w.becameReady.Delete(item.Key) @@ -316,7 +316,9 @@ func (w *priorityqueue[T]) GetWithPriority() (_ T, priority int, shutdown bool) return zero, 0, true } - w.waiters.Add(1) + w.lock.Lock() + w.waiters++ + w.lock.Unlock() w.notifyItemOrWaiterAdded() diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index fb186944ab..9c708e982b 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -441,6 +441,60 @@ func newQueueWithTimeForwarder() (_ *priorityqueue[string], _ *fakeMetricsProvid } } +func TestHighPriorityItemsAreReturnedBeforeLowPriorityItemMultipleTimes(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics := newQueue() + defer q.ShutDown() + + const itemsPerPriority = 1000 + lowPriority := 0 + lowMiddlePriority := 5 + middlePriority := 10 + upperMiddlePriority := 15 + highPriority := 20 + for i := range itemsPerPriority { + q.AddWithOpts(AddOpts{Priority: &highPriority}, fmt.Sprintf("high-%d", i)) + q.AddWithOpts(AddOpts{Priority: &upperMiddlePriority}, fmt.Sprintf("upperMiddle-%d", i)) + q.AddWithOpts(AddOpts{Priority: &middlePriority}, fmt.Sprintf("middle-%d", i)) + q.AddWithOpts(AddOpts{Priority: &lowMiddlePriority}, fmt.Sprintf("lowMiddle-%d", i)) + q.AddWithOpts(AddOpts{Priority: &lowPriority}, fmt.Sprintf("low-%d", i)) + } + synctest.Wait() + + for range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(highPriority)) + g.Expect(key).To(HavePrefix("high-")) + } + for range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(upperMiddlePriority)) + g.Expect(key).To(HavePrefix("upperMiddle-")) + } + for range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(middlePriority)) + g.Expect(key).To(HavePrefix("middle-")) + } + for range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(lowMiddlePriority)) + g.Expect(key).To(HavePrefix("lowMiddle-")) + } + for range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(lowPriority)) + g.Expect(key).To(HavePrefix("low-")) + } + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{10: 0, 5: 0, 0: 0, 20: 0, 15: 0})) + g.Expect(metrics.adds["test"]).To(Equal(itemsPerPriority * 5)) + g.Expect(metrics.retries["test"]).To(Equal(0)) + }) +} + func newQueue() (*priorityqueue[string], *fakeMetricsProvider) { metrics := newFakeMetricsProvider() q := New("test", func(o *Opts[string]) { From 0ddbc5205c01f30a1bc604e9e4857a22d6b57243 Mon Sep 17 00:00:00 2001 From: dongjiang Date: Thu, 30 Oct 2025 20:38:04 +0800 Subject: [PATCH 35/68] change sort to slices package (#3370) Signed-off-by: dongjiang --- pkg/cache/cache.go | 4 ++-- pkg/cache/cache_test.go | 14 +++++++------- pkg/client/apiutil/errors.go | 4 ++-- pkg/healthz/healthz.go | 4 ++-- pkg/internal/testing/certs/tinyca_test.go | 9 +++++---- pkg/internal/testing/process/arguments.go | 4 ++-- tools/setup-envtest/env/env.go | 9 +++++---- tools/setup-envtest/env/helpers.go | 7 ++++--- tools/setup-envtest/remote/http_client.go | 7 +++---- tools/setup-envtest/store/store.go | 17 +++++++++-------- tools/setup-envtest/versions/misc_test.go | 6 +++--- tools/setup-envtest/versions/version.go | 19 ++++++++++++++----- .../setup-envtest/workflows/workflows_test.go | 4 ++-- 13 files changed, 60 insertions(+), 48 deletions(-) diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index a7e491855a..107a7f1cda 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -22,7 +22,6 @@ import ( "maps" "net/http" "slices" - "sort" "time" corev1 "k8s.io/api/core/v1" @@ -583,7 +582,8 @@ func defaultConfig(toDefault, defaultFrom Config) Config { func namespaceAllSelector(namespaces []string) []fields.Selector { selectors := make([]fields.Selector, 0, len(namespaces)-1) - sort.Strings(namespaces) + slices.Sort(namespaces) + for _, namespace := range namespaces { if namespace != metav1.NamespaceAll { selectors = append(selectors, fields.OneTermNotEqualSelector("metadata.namespace", namespace)) diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 7748e2e317..c2dae0978f 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -21,7 +21,7 @@ import ( "errors" "fmt" "reflect" - "sort" + "slices" "strconv" "strings" "time" @@ -808,8 +808,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("verifying the pointer fields in pod have the same addresses") Expect(outList1.Items).To(HaveLen(len(outList2.Items))) - sort.SliceStable(outList1.Items, func(i, j int) bool { return outList1.Items[i].Name <= outList1.Items[j].Name }) - sort.SliceStable(outList2.Items, func(i, j int) bool { return outList2.Items[i].Name <= outList2.Items[j].Name }) + slices.SortStableFunc(outList1.Items, func(i, j corev1.Pod) int { return strings.Compare(i.Name, j.Name) }) + slices.SortStableFunc(outList2.Items, func(i, j corev1.Pod) int { return strings.Compare(i.Name, j.Name) }) for i := range outList1.Items { a := &outList1.Items[i] b := &outList2.Items[i] @@ -1134,8 +1134,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("verifying the pointer fields in pod have the same addresses") Expect(outList1.Items).To(HaveLen(len(outList2.Items))) - sort.SliceStable(outList1.Items, func(i, j int) bool { return outList1.Items[i].GetName() <= outList1.Items[j].GetName() }) - sort.SliceStable(outList2.Items, func(i, j int) bool { return outList2.Items[i].GetName() <= outList2.Items[j].GetName() }) + slices.SortStableFunc(outList1.Items, func(i, j unstructured.Unstructured) int { return strings.Compare(i.GetName(), j.GetName()) }) + slices.SortStableFunc(outList2.Items, func(i, j unstructured.Unstructured) int { return strings.Compare(i.GetName(), j.GetName()) }) for i := range outList1.Items { a := &outList1.Items[i] b := &outList2.Items[i] @@ -1519,8 +1519,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("verifying the pointer fields in pod have the same addresses") Expect(outList1.Items).To(HaveLen(len(outList2.Items))) - sort.SliceStable(outList1.Items, func(i, j int) bool { return outList1.Items[i].Name <= outList1.Items[j].Name }) - sort.SliceStable(outList2.Items, func(i, j int) bool { return outList2.Items[i].Name <= outList2.Items[j].Name }) + slices.SortStableFunc(outList1.Items, func(i, j metav1.PartialObjectMetadata) int { return strings.Compare(i.Name, j.Name) }) + slices.SortStableFunc(outList2.Items, func(i, j metav1.PartialObjectMetadata) int { return strings.Compare(i.Name, j.Name) }) for i := range outList1.Items { a := &outList1.Items[i] b := &outList2.Items[i] diff --git a/pkg/client/apiutil/errors.go b/pkg/client/apiutil/errors.go index c216c49d2a..b00e071232 100644 --- a/pkg/client/apiutil/errors.go +++ b/pkg/client/apiutil/errors.go @@ -18,7 +18,7 @@ package apiutil import ( "fmt" - "sort" + "slices" "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -38,7 +38,7 @@ func (e *ErrResourceDiscoveryFailed) Error() string { for k, v := range *e { subErrors = append(subErrors, fmt.Sprintf("%s: %v", k, v)) } - sort.Strings(subErrors) + slices.Sort(subErrors) return fmt.Sprintf("unable to retrieve the complete list of server APIs: %s", strings.Join(subErrors, ", ")) } diff --git a/pkg/healthz/healthz.go b/pkg/healthz/healthz.go index cfb5dc8d02..149b02ec98 100644 --- a/pkg/healthz/healthz.go +++ b/pkg/healthz/healthz.go @@ -20,7 +20,7 @@ import ( "fmt" "net/http" "path" - "sort" + "slices" "strings" "k8s.io/apimachinery/pkg/util/sets" @@ -75,7 +75,7 @@ func (h *Handler) serveAggregated(resp http.ResponseWriter, req *http.Request) { } // ...sort to be consistent... - sort.Slice(parts, func(i, j int) bool { return parts[i].name < parts[j].name }) + slices.SortStableFunc(parts, func(i, j checkStatus) int { return strings.Compare(i.name, j.name) }) // ...and write out the result // TODO(directxman12): this should also accept a request for JSON content (via a accept header) diff --git a/pkg/internal/testing/certs/tinyca_test.go b/pkg/internal/testing/certs/tinyca_test.go index 5d84de56fb..9542975565 100644 --- a/pkg/internal/testing/certs/tinyca_test.go +++ b/pkg/internal/testing/certs/tinyca_test.go @@ -21,7 +21,7 @@ import ( "encoding/pem" "math/big" "net" - "sort" + "slices" "time" . "github.com/onsi/ginkgo/v2" @@ -67,10 +67,11 @@ var _ = Describe("TinyCA", func() { secondCerts.Cert.SerialNumber, thirdCerts.Cert.SerialNumber, } - // quick uniqueness check of numbers: sort, then you only have to compare sequential entries - sort.Slice(serials, func(i, j int) bool { - return serials[i].Cmp(serials[j]) == -1 + // quick uniqueness check of numbers: slices sort, then you only have to compare sequential entries + slices.SortStableFunc(serials, func(i, j *big.Int) int { + return i.Cmp(j) }) + Expect(serials[1].Cmp(serials[0])).NotTo(Equal(0), "serials shouldn't be equal") Expect(serials[2].Cmp(serials[1])).NotTo(Equal(0), "serials shouldn't be equal") }) diff --git a/pkg/internal/testing/process/arguments.go b/pkg/internal/testing/process/arguments.go index 391eec1fac..1e556e9980 100644 --- a/pkg/internal/testing/process/arguments.go +++ b/pkg/internal/testing/process/arguments.go @@ -19,7 +19,7 @@ package process import ( "bytes" "html/template" - "sort" + "slices" "strings" ) @@ -230,7 +230,7 @@ func (a *Arguments) AsStrings(defaults map[string][]string) []string { for key := range a.values { keysInOrder = append(keysInOrder, key) } - sort.Strings(keysInOrder) + slices.Sort(keysInOrder) var res []string for _, key := range keysInOrder { diff --git a/tools/setup-envtest/env/env.go b/tools/setup-envtest/env/env.go index 6168739eb6..cfcad3505b 100644 --- a/tools/setup-envtest/env/env.go +++ b/tools/setup-envtest/env/env.go @@ -10,7 +10,7 @@ import ( "io" "io/fs" "path/filepath" - "sort" + "slices" "strings" "text/tabwriter" @@ -121,9 +121,10 @@ func (e *Env) ListVersions(ctx context.Context) { if !e.Version.Matches(set.Version) { continue } - sort.Slice(set.Platforms, func(i, j int) bool { - return orderPlatforms(set.Platforms[i].Platform, set.Platforms[j].Platform) + slices.SortStableFunc(set.Platforms, func(i, j versions.PlatformItem) int { + return orderPlatforms(i.Platform, j.Platform) }) + for _, plat := range set.Platforms { if e.Platform.Matches(plat.Platform) { fmt.Fprintf(out, "(available)\tv%s\t%s\n", set.Version, plat) @@ -246,7 +247,7 @@ func (e *Env) EnsureVersionIsSet(ctx context.Context) { serverVer, platform := e.LatestVersion(ctx) // if we're not forcing a download, and we have a newer local version, just use that - if !e.ForceDownload && localVer != nil && localVer.NewerThan(serverVer) { + if !e.ForceDownload && localVer != nil && localVer.Compare(serverVer) > 0 { e.Platform.Platform = localPlat // update our data with hash e.Version.MakeConcrete(*localVer) return diff --git a/tools/setup-envtest/env/helpers.go b/tools/setup-envtest/env/helpers.go index 2c98c88d95..8572088517 100644 --- a/tools/setup-envtest/env/helpers.go +++ b/tools/setup-envtest/env/helpers.go @@ -5,17 +5,18 @@ package env import ( "fmt" + "strings" "sigs.k8s.io/controller-runtime/tools/setup-envtest/versions" ) // orderPlatforms orders platforms by OS then arch. -func orderPlatforms(first, second versions.Platform) bool { +func orderPlatforms(first, second versions.Platform) int { // sort by OS, then arch if first.OS != second.OS { - return first.OS < second.OS + return strings.Compare(first.OS, second.OS) } - return first.Arch < second.Arch + return strings.Compare(first.Arch, second.Arch) } // PrintFormat indicates how to print out fetch and switch results. diff --git a/tools/setup-envtest/remote/http_client.go b/tools/setup-envtest/remote/http_client.go index 0339654a82..a87ef1f105 100644 --- a/tools/setup-envtest/remote/http_client.go +++ b/tools/setup-envtest/remote/http_client.go @@ -9,7 +9,7 @@ import ( "io" "net/http" "net/url" - "sort" + "slices" "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/tools/setup-envtest/versions" @@ -87,9 +87,8 @@ func (c *HTTPClient) ListVersions(ctx context.Context) ([]versions.Set, error) { res = append(res, versions.Set{Version: ver, Platforms: details}) } // sort in inverse order so that the newest one is first - sort.Slice(res, func(i, j int) bool { - first, second := res[i].Version, res[j].Version - return first.NewerThan(second) + slices.SortStableFunc(res, func(i, j versions.Set) int { + return i.Version.Compare(j.Version) }) return res, nil diff --git a/tools/setup-envtest/store/store.go b/tools/setup-envtest/store/store.go index bb5a1f7bcd..1e1d4beb3c 100644 --- a/tools/setup-envtest/store/store.go +++ b/tools/setup-envtest/store/store.go @@ -12,7 +12,8 @@ import ( "io" "os" "path/filepath" - "sort" + "slices" + "strings" "github.com/go-logr/logr" "github.com/spf13/afero" @@ -102,11 +103,11 @@ func (s *Store) List(ctx context.Context, matching Filter) ([]Item, error) { return nil, fmt.Errorf("unable to list version-platform pairs in store: %w", err) } - sort.Slice(res, func(i, j int) bool { - if !res[i].Version.Matches(res[j].Version) { - return res[i].Version.NewerThan(res[j].Version) + slices.SortStableFunc(res, func(i, j Item) int { + if !i.Version.Matches(j.Version) { + return i.Version.Compare(j.Version) } - return orderPlatforms(res[i].Platform, res[j].Platform) + return orderPlatforms(i.Platform, j.Platform) }) return res, nil @@ -296,10 +297,10 @@ func (s *Store) removeItem(itemDir afero.Fs) error { } // orderPlatforms orders platforms by OS then arch. -func orderPlatforms(first, second versions.Platform) bool { +func orderPlatforms(first, second versions.Platform) int { // sort by OS, then arch if first.OS != second.OS { - return first.OS < second.OS + return strings.Compare(first.OS, second.OS) } - return first.Arch < second.Arch + return strings.Compare(first.Arch, second.Arch) } diff --git a/tools/setup-envtest/versions/misc_test.go b/tools/setup-envtest/versions/misc_test.go index a609f4dc60..1c0dbb68db 100644 --- a/tools/setup-envtest/versions/misc_test.go +++ b/tools/setup-envtest/versions/misc_test.go @@ -36,13 +36,13 @@ var _ = Describe("Concrete", func() { Describe("when ordering relative to other versions", func() { ver1163 := Concrete{Major: 1, Minor: 16, Patch: 3} Specify("newer patch should be newer", func() { - Expect(ver1163.NewerThan(Concrete{Major: 1, Minor: 16})).To(BeTrue()) + Expect(ver1163.Compare(Concrete{Major: 1, Minor: 16})).To(Equal(1)) }) Specify("newer minor should be newer", func() { - Expect(ver1163.NewerThan(Concrete{Major: 1, Minor: 15, Patch: 3})).To(BeTrue()) + Expect(ver1163.Compare(Concrete{Major: 1, Minor: 15, Patch: 3})).To(Equal(1)) }) Specify("newer major should be newer", func() { - Expect(ver1163.NewerThan(Concrete{Major: 0, Minor: 16, Patch: 3})).To(BeTrue()) + Expect(ver1163.Compare(Concrete{Major: 0, Minor: 16, Patch: 3})).To(Equal(1)) }) }) }) diff --git a/tools/setup-envtest/versions/version.go b/tools/setup-envtest/versions/version.go index 945a95006f..bad05c3218 100644 --- a/tools/setup-envtest/versions/version.go +++ b/tools/setup-envtest/versions/version.go @@ -27,15 +27,24 @@ func (c Concrete) AsConcrete() *Concrete { return &c } -// NewerThan checks if the given other version is newer than this one. -func (c Concrete) NewerThan(other Concrete) bool { +// Compare checks if the given other version is newer than this one. +func (c Concrete) Compare(other Concrete) int { + IntCompare := func(a, b int) int { + if a > b { + return 1 + } else if a < b { + return -1 + } + return 0 + } + if c.Major != other.Major { - return c.Major > other.Major + return IntCompare(c.Major, other.Major) } if c.Minor != other.Minor { - return c.Minor > other.Minor + return IntCompare(c.Minor, other.Minor) } - return c.Patch > other.Patch + return IntCompare(c.Patch, other.Patch) } // Matches checks if this version is equal to the other one. diff --git a/tools/setup-envtest/workflows/workflows_test.go b/tools/setup-envtest/workflows/workflows_test.go index 435ae24285..eaa1c8c0a3 100644 --- a/tools/setup-envtest/workflows/workflows_test.go +++ b/tools/setup-envtest/workflows/workflows_test.go @@ -9,7 +9,7 @@ import ( "io/fs" "path/filepath" "runtime/debug" - "sort" + "slices" "strings" . "github.com/onsi/ginkgo/v2" @@ -333,7 +333,7 @@ var _ = Describe("Workflows", func() { archiveNames = append(archiveNames, archiveName) } } - sort.Strings(archiveNames) + slices.Sort(archiveNames) archiveNamesSet := sets.Set[string]{}.Insert(archiveNames[:7]...) // Delete all other archives for _, release := range remoteHTTPItems.index.Releases { From b4232f09de39f35320837a8dee0d0e8c9cd73e74 Mon Sep 17 00:00:00 2001 From: Ming Zhao Date: Thu, 30 Oct 2025 09:36:22 -0700 Subject: [PATCH 36/68] envtest: respect pre-configured binary paths in ControlPlane This commit fixes an issue where Environment.Start() would ignore pre-configured binary paths (APIServer.Path, Etcd.Path, KubectlPath) set in ControlPlane when DownloadBinaryAssets is false. Changes: - Extract path configuration logic into configureBinaryPaths() method for better testability and separation of concerns - Only auto-configure binary paths when they are empty (not pre-set) - When DownloadBinaryAssets is true, downloaded paths are still used (preserving existing behavior) - Update ControlPlane field documentation to clarify path behavior - Add tests in envtest_test.go to verify path handling logic --- pkg/envtest/envtest_test.go | 52 ++++++++++++++++++++++++++++++ pkg/envtest/server.go | 63 ++++++++++++++++++++++++++----------- 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/pkg/envtest/envtest_test.go b/pkg/envtest/envtest_test.go index ce3e9a4d3f..806c9f43cc 100644 --- a/pkg/envtest/envtest_test.go +++ b/pkg/envtest/envtest_test.go @@ -963,4 +963,56 @@ var _ = Describe("Test", func() { Expect(env.WebhookInstallOptions.LocalServingCertDir).ShouldNot(BeADirectory()) }) }) + + Describe("Binary Path Handling", func() { + It("should respect pre-configured binary paths when not downloading", func() { + // Setup custom paths + customAPIServerPath := "/custom/path/to/kube-apiserver" + customEtcdPath := "/custom/path/to/etcd" + customKubectlPath := "/custom/path/to/kubectl" + + // Create an environment with pre-configured paths + testEnv := &Environment{} + testEnv.ControlPlane.GetAPIServer().Path = customAPIServerPath + testEnv.ControlPlane.Etcd = &Etcd{} + testEnv.ControlPlane.Etcd.Path = customEtcdPath + testEnv.ControlPlane.KubectlPath = customKubectlPath + + // Set BinaryAssetsDirectory to ensure it's not using defaults + testEnv.BinaryAssetsDirectory = "/should/not/be/used" + testEnv.DownloadBinaryAssets = false + + // Call configureBinaryPaths to test the path configuration logic + err := testEnv.configureBinaryPaths() + Expect(err).NotTo(HaveOccurred()) + + // Verify paths were preserved (not overwritten) + apiServer := testEnv.ControlPlane.GetAPIServer() + Expect(apiServer.Path).To(Equal(customAPIServerPath)) + Expect(testEnv.ControlPlane.Etcd.Path).To(Equal(customEtcdPath)) + Expect(testEnv.ControlPlane.KubectlPath).To(Equal(customKubectlPath)) + }) + + It("should auto-configure binary paths when not pre-configured", func() { + // Create an environment without pre-configured paths + testEnv := &Environment{} + testEnv.BinaryAssetsDirectory = "/test/assets" + testEnv.DownloadBinaryAssets = false + + // Call configureBinaryPaths + err := testEnv.configureBinaryPaths() + Expect(err).NotTo(HaveOccurred()) + + // Verify paths were set using BinPathFinder + apiServer := testEnv.ControlPlane.GetAPIServer() + Expect(apiServer.Path).NotTo(BeEmpty()) + Expect(testEnv.ControlPlane.Etcd.Path).NotTo(BeEmpty()) + Expect(testEnv.ControlPlane.KubectlPath).NotTo(BeEmpty()) + + // Verify the paths contain the binary names + Expect(apiServer.Path).To(ContainSubstring("kube-apiserver")) + Expect(testEnv.ControlPlane.Etcd.Path).To(ContainSubstring("etcd")) + Expect(testEnv.ControlPlane.KubectlPath).To(ContainSubstring("kubectl")) + }) + }) }) diff --git a/pkg/envtest/server.go b/pkg/envtest/server.go index 9bb81ed2ab..c9f19da977 100644 --- a/pkg/envtest/server.go +++ b/pkg/envtest/server.go @@ -109,7 +109,11 @@ var ( // Environment creates a Kubernetes test environment that will start / stop the Kubernetes control plane and // install extension APIs. type Environment struct { - // ControlPlane is the ControlPlane including the apiserver and etcd + // ControlPlane is the ControlPlane including the apiserver and etcd. + // Binary paths (APIServer.Path, Etcd.Path, KubectlPath) can be pre-configured in ControlPlane. + // If DownloadBinaryAssets is true, the downloaded paths will always be used. + // If DownloadBinaryAssets is false and paths are not pre-configured (default is empty), they will be + // automatically resolved using BinaryAssetsDirectory. ControlPlane controlplane.ControlPlane // Scheme is used to determine if conversion webhooks should be enabled @@ -211,6 +215,40 @@ func (te *Environment) Stop() error { return te.ControlPlane.Stop() } +// configureBinaryPaths configures the binary paths for the API server, etcd, and kubectl. +// If DownloadBinaryAssets is true, it downloads and uses those paths. +// If DownloadBinaryAssets is false, it only sets paths that are not already configured (empty). +func (te *Environment) configureBinaryPaths() error { + apiServer := te.ControlPlane.GetAPIServer() + + if te.ControlPlane.Etcd == nil { + te.ControlPlane.Etcd = &controlplane.Etcd{} + } + + if te.DownloadBinaryAssets { + apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.TODO(), + te.BinaryAssetsDirectory, te.DownloadBinaryAssetsVersion, te.DownloadBinaryAssetsIndexURL) + if err != nil { + return err + } + + apiServer.Path = apiServerPath + te.ControlPlane.Etcd.Path = etcdPath + te.ControlPlane.KubectlPath = kubectlPath + } else { + if apiServer.Path == "" { + apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory) + } + if te.ControlPlane.Etcd.Path == "" { + te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory) + } + if te.ControlPlane.KubectlPath == "" { + te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory) + } + } + return nil +} + // Start starts a local Kubernetes server and updates te.ApiserverPort with the port it is listening on. func (te *Environment) Start() (*rest.Config, error) { if te.useExistingCluster() { @@ -229,10 +267,6 @@ func (te *Environment) Start() (*rest.Config, error) { } else { apiServer := te.ControlPlane.GetAPIServer() - if te.ControlPlane.Etcd == nil { - te.ControlPlane.Etcd = &controlplane.Etcd{} - } - if os.Getenv(envAttachOutput) == "true" { te.AttachControlPlaneOutput = true } @@ -243,6 +277,9 @@ func (te *Environment) Start() (*rest.Config, error) { if apiServer.Err == nil { apiServer.Err = os.Stderr } + if te.ControlPlane.Etcd == nil { + te.ControlPlane.Etcd = &controlplane.Etcd{} + } if te.ControlPlane.Etcd.Out == nil { te.ControlPlane.Etcd.Out = os.Stdout } @@ -251,20 +288,8 @@ func (te *Environment) Start() (*rest.Config, error) { } } - if te.DownloadBinaryAssets { - apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.TODO(), - te.BinaryAssetsDirectory, te.DownloadBinaryAssetsVersion, te.DownloadBinaryAssetsIndexURL) - if err != nil { - return nil, err - } - - apiServer.Path = apiServerPath - te.ControlPlane.Etcd.Path = etcdPath - te.ControlPlane.KubectlPath = kubectlPath - } else { - apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory) - te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory) - te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory) + if err := te.configureBinaryPaths(); err != nil { + return nil, fmt.Errorf("failed to configure binary paths: %w", err) } if err := te.defaultTimeouts(); err != nil { From 9d4a45c3c3293aeae0df3e5b94587e73d002a523 Mon Sep 17 00:00:00 2001 From: dongjiang Date: Fri, 31 Oct 2025 19:16:04 +0800 Subject: [PATCH 37/68] add golangci lint rule Signed-off-by: dongjiang --- .golangci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 5f8edd56b4..88fa35359e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,6 +11,7 @@ linters: - bidichk - bodyclose - copyloopvar + - depguard - dogsled - dupl - errcheck @@ -42,6 +43,12 @@ linters: - unused - whitespace settings: + depguard: + rules: + forbid-pkg-errors: + deny: + - pkg: sort + desc: Should be replaced with slices package forbidigo: forbid: - pattern: context.Background From 0f049b81480d75c53d33053cc7b968d06ee30f50 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 31 Oct 2025 09:10:03 -0400 Subject: [PATCH 38/68] :warning: Generic Validator and Defaulter (#3360) * Type defaulter and validator * Builder * Introduce WebhookFor * Update existing WebhookManagedBy * Linting * Preserve alias for NewWebhookManagedBy * Use WithDefaulter/WithValidator going forward * Test defaulter * TestValidatorBuilder * Mixed * Custom and Typed validator/defaulter are mutually exclusive * Type new in validator * Simplify builder * Re-add aliases * feedback --- alias.go | 9 +- examples/builtins/main.go | 3 +- examples/builtins/mutatingwebhook.go | 8 +- examples/builtins/validatingwebhook.go | 13 +- examples/crd/main.go | 3 +- pkg/builder/example_webhook_test.go | 3 +- pkg/builder/webhook.go | 128 ++- pkg/builder/webhook_test.go | 1274 ++++++++++++--------- pkg/webhook/admission/defaulter_custom.go | 57 +- pkg/webhook/admission/validator_custom.go | 58 +- pkg/webhook/alias.go | 2 + 11 files changed, 885 insertions(+), 673 deletions(-) diff --git a/alias.go b/alias.go index 01ba012dcc..dde2c5b930 100644 --- a/alias.go +++ b/alias.go @@ -18,6 +18,7 @@ package controllerruntime import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client/config" @@ -104,9 +105,6 @@ var ( // NewControllerManagedBy returns a new controller builder that will be started by the provided Manager. NewControllerManagedBy = builder.ControllerManagedBy - // NewWebhookManagedBy returns a new webhook builder that will be started by the provided Manager. - NewWebhookManagedBy = builder.WebhookManagedBy - // NewManager returns a new Manager for creating Controllers. // Note that if ContentType in the given config is not set, "application/vnd.kubernetes.protobuf" // will be used for all built-in resources of Kubernetes, and "application/json" is for other types @@ -155,3 +153,8 @@ var ( // SetLogger sets a concrete logging implementation for all deferred Loggers. SetLogger = log.SetLogger ) + +// NewWebhookManagedBy returns a new webhook builder for the provided type T. +func NewWebhookManagedBy[T runtime.Object](mgr manager.Manager, obj T) *builder.WebhookBuilder[T] { + return builder.WebhookManagedBy(mgr, obj) +} diff --git a/examples/builtins/main.go b/examples/builtins/main.go index 3a47814d8c..f30c652583 100644 --- a/examples/builtins/main.go +++ b/examples/builtins/main.go @@ -60,8 +60,7 @@ func main() { os.Exit(1) } - if err := ctrl.NewWebhookManagedBy(mgr). - For(&corev1.Pod{}). + if err := ctrl.NewWebhookManagedBy(mgr, &corev1.Pod{}). WithDefaulter(&podAnnotator{}). WithValidator(&podValidator{}). Complete(); err != nil { diff --git a/examples/builtins/mutatingwebhook.go b/examples/builtins/mutatingwebhook.go index a588eba8f9..0f150c9b6c 100644 --- a/examples/builtins/mutatingwebhook.go +++ b/examples/builtins/mutatingwebhook.go @@ -18,10 +18,8 @@ package main import ( "context" - "fmt" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -31,12 +29,8 @@ import ( // podAnnotator annotates Pods type podAnnotator struct{} -func (a *podAnnotator) Default(ctx context.Context, obj runtime.Object) error { +func (a *podAnnotator) Default(ctx context.Context, pod *corev1.Pod) error { log := logf.FromContext(ctx) - pod, ok := obj.(*corev1.Pod) - if !ok { - return fmt.Errorf("expected a Pod but got a %T", obj) - } if pod.Annotations == nil { pod.Annotations = map[string]string{} diff --git a/examples/builtins/validatingwebhook.go b/examples/builtins/validatingwebhook.go index 1bee7f7c84..eb08159688 100644 --- a/examples/builtins/validatingwebhook.go +++ b/examples/builtins/validatingwebhook.go @@ -21,7 +21,6 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -33,12 +32,8 @@ import ( type podValidator struct{} // validate admits a pod if a specific annotation exists. -func (v *podValidator) validate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *podValidator) validate(ctx context.Context, pod *corev1.Pod) (admission.Warnings, error) { log := logf.FromContext(ctx) - pod, ok := obj.(*corev1.Pod) - if !ok { - return nil, fmt.Errorf("expected a Pod but got a %T", obj) - } log.Info("Validating Pod") key := "example-mutating-admission-webhook" @@ -53,14 +48,14 @@ func (v *podValidator) validate(ctx context.Context, obj runtime.Object) (admiss return nil, nil } -func (v *podValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *podValidator) ValidateCreate(ctx context.Context, obj *corev1.Pod) (admission.Warnings, error) { return v.validate(ctx, obj) } -func (v *podValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { +func (v *podValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *corev1.Pod) (admission.Warnings, error) { return v.validate(ctx, newObj) } -func (v *podValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *podValidator) ValidateDelete(ctx context.Context, obj *corev1.Pod) (admission.Warnings, error) { return v.validate(ctx, obj) } diff --git a/examples/crd/main.go b/examples/crd/main.go index 0bf65c9890..eec58abc01 100644 --- a/examples/crd/main.go +++ b/examples/crd/main.go @@ -129,8 +129,7 @@ func main() { os.Exit(1) } - err = ctrl.NewWebhookManagedBy(mgr). - For(&api.ChaosPod{}). + err = ctrl.NewWebhookManagedBy(mgr, &api.ChaosPod{}). Complete() if err != nil { setupLog.Error(err, "unable to create webhook") diff --git a/pkg/builder/example_webhook_test.go b/pkg/builder/example_webhook_test.go index c26eba8a13..133da47272 100644 --- a/pkg/builder/example_webhook_test.go +++ b/pkg/builder/example_webhook_test.go @@ -40,8 +40,7 @@ func ExampleWebhookBuilder() { } err = builder. - WebhookManagedBy(mgr). // Create the WebhookManagedBy - For(&examplegroup.ChaosPod{}). // ChaosPod is a CRD. + WebhookManagedBy(mgr, &examplegroup.ChaosPod{}). // Create the WebhookManagedBy Complete() if err != nil { log.Error(err, "could not create webhook") diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go index bb5b6deb56..428100a66c 100644 --- a/pkg/builder/webhook.go +++ b/pkg/builder/webhook.go @@ -37,11 +37,13 @@ import ( ) // WebhookBuilder builds a Webhook. -type WebhookBuilder struct { +type WebhookBuilder[T runtime.Object] struct { apiType runtime.Object customDefaulter admission.CustomDefaulter + defaulter admission.Defaulter[T] customDefaulterOpts []admission.DefaulterOption customValidator admission.CustomValidator + validator admission.Validator[T] customPath string customValidatorCustomPath string customDefaulterCustomPath string @@ -56,59 +58,61 @@ type WebhookBuilder struct { } // WebhookManagedBy returns a new webhook builder. -func WebhookManagedBy(m manager.Manager) *WebhookBuilder { - return &WebhookBuilder{mgr: m} +func WebhookManagedBy[T runtime.Object](m manager.Manager, object T) *WebhookBuilder[T] { + return &WebhookBuilder[T]{mgr: m, apiType: object} } -// TODO(droot): update the GoDoc for conversion. - -// For takes a runtime.Object which should be a CR. -// If the given object implements the admission.Defaulter interface, a MutatingWebhook will be wired for this type. -// If the given object implements the admission.Validator interface, a ValidatingWebhook will be wired for this type. -func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder { - if blder.apiType != nil { - blder.err = errors.New("For(...) should only be called once, could not assign multiple objects for webhook registration") - } - blder.apiType = apiType +// WithCustomDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption) +// will be wired for this type. +// Deprecated: Use WithDefaulter instead. +func (blder *WebhookBuilder[T]) WithCustomDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder[T] { + blder.customDefaulter = defaulter + blder.customDefaulterOpts = opts return blder } -// WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption) -// will be wired for this type. -func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder { - blder.customDefaulter = defaulter +// WithDefaulter sets up the provided admission.Defaulter in a defaulting webhook. +func (blder *WebhookBuilder[T]) WithDefaulter(defaulter admission.Defaulter[T], opts ...admission.DefaulterOption) *WebhookBuilder[T] { + blder.defaulter = defaulter blder.customDefaulterOpts = opts return blder } -// WithValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type. -func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator) *WebhookBuilder { +// WithCustomValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type. +// Deprecated: Use WithValidator instead. +func (blder *WebhookBuilder[T]) WithCustomValidator(validator admission.CustomValidator) *WebhookBuilder[T] { blder.customValidator = validator return blder } +// WithValidator sets up the provided admission.Validator in a validating webhook. +func (blder *WebhookBuilder[T]) WithValidator(validator admission.Validator[T]) *WebhookBuilder[T] { + blder.validator = validator + return blder +} + // WithConverter takes a func that constructs a converter.Converter. -// The Converter will then be used by the conversion endpoint for the type passed into For(). -func (blder *WebhookBuilder) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder { +// The Converter will then be used by the conversion endpoint for the type passed into NewWebhookManagedBy() +func (blder *WebhookBuilder[T]) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder[T] { blder.converterConstructor = converterConstructor return blder } // WithLogConstructor overrides the webhook's LogConstructor. -func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder[T] { blder.logConstructor = logConstructor return blder } // WithContextFunc overrides the webhook's WithContextFunc. -func (blder *WebhookBuilder) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder[T] { blder.contextFunc = contextFunc return blder } // RecoverPanic indicates whether panics caused by the webhook should be recovered. // Defaults to true. -func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder { +func (blder *WebhookBuilder[T]) RecoverPanic(recoverPanic bool) *WebhookBuilder[T] { blder.recoverPanic = &recoverPanic return blder } @@ -117,25 +121,25 @@ func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder { // // Deprecated: WithCustomPath should not be used anymore. // Please use WithValidatorCustomPath or WithDefaulterCustomPath instead. -func (blder *WebhookBuilder) WithCustomPath(customPath string) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithCustomPath(customPath string) *WebhookBuilder[T] { blder.customPath = customPath return blder } // WithValidatorCustomPath overrides the path of the Validator. -func (blder *WebhookBuilder) WithValidatorCustomPath(customPath string) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithValidatorCustomPath(customPath string) *WebhookBuilder[T] { blder.customValidatorCustomPath = customPath return blder } // WithDefaulterCustomPath overrides the path of the Defaulter. -func (blder *WebhookBuilder) WithDefaulterCustomPath(customPath string) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithDefaulterCustomPath(customPath string) *WebhookBuilder[T] { blder.customDefaulterCustomPath = customPath return blder } // Complete builds the webhook. -func (blder *WebhookBuilder) Complete() error { +func (blder *WebhookBuilder[T]) Complete() error { // Set the Config blder.loadRestConfig() @@ -146,13 +150,13 @@ func (blder *WebhookBuilder) Complete() error { return blder.registerWebhooks() } -func (blder *WebhookBuilder) loadRestConfig() { +func (blder *WebhookBuilder[T]) loadRestConfig() { if blder.config == nil { blder.config = blder.mgr.GetConfig() } } -func (blder *WebhookBuilder) setLogConstructor() { +func (blder *WebhookBuilder[T]) setLogConstructor() { if blder.logConstructor == nil { blder.logConstructor = func(base logr.Logger, req *admission.Request) logr.Logger { log := base.WithValues( @@ -172,11 +176,11 @@ func (blder *WebhookBuilder) setLogConstructor() { } } -func (blder *WebhookBuilder) isThereCustomPathConflict() bool { +func (blder *WebhookBuilder[T]) isThereCustomPathConflict() bool { return (blder.customPath != "" && blder.customDefaulter != nil && blder.customValidator != nil) || (blder.customPath != "" && blder.customDefaulterCustomPath != "") || (blder.customPath != "" && blder.customValidatorCustomPath != "") } -func (blder *WebhookBuilder) registerWebhooks() error { +func (blder *WebhookBuilder[T]) registerWebhooks() error { typ, err := blder.getType() if err != nil { return err @@ -217,8 +221,11 @@ func (blder *WebhookBuilder) registerWebhooks() error { } // registerDefaultingWebhook registers a defaulting webhook if necessary. -func (blder *WebhookBuilder) registerDefaultingWebhook() error { - mwh := blder.getDefaultingWebhook() +func (blder *WebhookBuilder[T]) registerDefaultingWebhook() error { + mwh, err := blder.getDefaultingWebhook() + if err != nil { + return err + } if mwh != nil { mwh.LogConstructor = blder.logConstructor mwh.WithContextFunc = blder.contextFunc @@ -244,20 +251,28 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() error { return nil } -func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook { - if defaulter := blder.customDefaulter; defaulter != nil { - w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter, blder.customDefaulterOpts...) - if blder.recoverPanic != nil { - w = w.WithRecoverPanic(*blder.recoverPanic) +func (blder *WebhookBuilder[T]) getDefaultingWebhook() (*admission.Webhook, error) { + var w *admission.Webhook + if blder.defaulter != nil { + if blder.customDefaulter != nil { + return nil, errors.New("only one of Defaulter or CustomDefaulter can be set") } - return w + w = admission.WithDefaulter(blder.mgr.GetScheme(), blder.defaulter, blder.customDefaulterOpts...) + } else if blder.customDefaulter != nil { + w = admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, blder.customDefaulter, blder.customDefaulterOpts...) } - return nil + if w != nil && blder.recoverPanic != nil { + w = w.WithRecoverPanic(*blder.recoverPanic) + } + return w, nil } // registerValidatingWebhook registers a validating webhook if necessary. -func (blder *WebhookBuilder) registerValidatingWebhook() error { - vwh := blder.getValidatingWebhook() +func (blder *WebhookBuilder[T]) registerValidatingWebhook() error { + vwh, err := blder.getValidatingWebhook() + if err != nil { + return err + } if vwh != nil { vwh.LogConstructor = blder.logConstructor vwh.WithContextFunc = blder.contextFunc @@ -283,18 +298,23 @@ func (blder *WebhookBuilder) registerValidatingWebhook() error { return nil } -func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook { - if validator := blder.customValidator; validator != nil { - w := admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, validator) - if blder.recoverPanic != nil { - w = w.WithRecoverPanic(*blder.recoverPanic) +func (blder *WebhookBuilder[T]) getValidatingWebhook() (*admission.Webhook, error) { + var w *admission.Webhook + if blder.validator != nil { + if blder.customValidator != nil { + return nil, errors.New("only one of Validator or CustomValidator can be set") } - return w + w = admission.WithValidator(blder.mgr.GetScheme(), blder.validator) + } else if blder.customValidator != nil { + w = admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, blder.customValidator) } - return nil + if w != nil && blder.recoverPanic != nil { + w = w.WithRecoverPanic(*blder.recoverPanic) + } + return w, nil } -func (blder *WebhookBuilder) registerConversionWebhook() error { +func (blder *WebhookBuilder[T]) registerConversionWebhook() error { if blder.converterConstructor != nil { converter, err := blder.converterConstructor(blder.mgr.GetScheme()) if err != nil { @@ -323,14 +343,14 @@ func (blder *WebhookBuilder) registerConversionWebhook() error { return nil } -func (blder *WebhookBuilder) getType() (runtime.Object, error) { +func (blder *WebhookBuilder[T]) getType() (runtime.Object, error) { if blder.apiType != nil { return blder.apiType, nil } - return nil, errors.New("For() must be called with a valid object") + return nil, errors.New("NewWebhookManagedBy() must be called with a valid object") } -func (blder *WebhookBuilder) isAlreadyHandled(path string) bool { +func (blder *WebhookBuilder[T]) isAlreadyHandled(path string) bool { if blder.mgr.GetWebhookServer().WebhookMux() == nil { return false } diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go index 72538ef7bf..e10e693ab8 100644 --- a/pkg/builder/webhook_test.go +++ b/pkg/builder/webhook_test.go @@ -85,35 +85,36 @@ func runTests(admissionReviewVersion string) { close(stop) }) - It("should scaffold a custom defaulting webhook", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("scaffold a defaulting webhook", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} - builder.Register(&TestDefaulter{}, &TestDefaulterList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - WithDefaulter(&TestCustomDefaulter{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestDefaulter" + "kind":"TestDefaulterObject" }, "resource":{ "group":"foo.test.org", @@ -130,68 +131,76 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a mutating webhook path") + path := generateMutatePath(testDefaulterGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"patch":`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook path that doesn't exist") + path = generateValidatePath(testDefaulterGVK) + _, err = reader.Seek(0, 0) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a mutating webhook path") - path := generateMutatePath(testDefaulterGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook path that doesn't exist") - path = generateValidatePath(testDefaulterGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - }) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + }, + Entry("Custom Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithCustomDefaulter(&TestCustomDefaulter{}) + }), + Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithDefaulter(&testDefaulter{}) + }), + ) - It("should scaffold a custom defaulting webhook with a custom path", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom defaulting webhook with a custom path", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} - builder.Register(&TestDefaulter{}, &TestDefaulterList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - customPath := "/custom-defaulting-path" - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - WithDefaulter(&TestCustomDefaulter{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - WithDefaulterCustomPath(customPath). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + customPath := "/custom-defaulting-path" + webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + WithDefaulterCustomPath(customPath). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestDefaulter" + "kind":"TestDefaulterObject" }, "resource":{ "group":"foo.test.org", @@ -208,66 +217,73 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } - By("sending a request to a mutating webhook path that have been overriten by a custom path") - path, err := generateCustomPath(customPath) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a mutating webhook path") - path = generateMutatePath(testDefaulterGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - }) + By("sending a request to a mutating webhook path that have been overriten by a custom path") + path, err := generateCustomPath(customPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"patch":`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a mutating webhook path") + path = generateMutatePath(testDefaulterGVK) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + }, + Entry("Custom Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithCustomDefaulter(&TestCustomDefaulter{}) + }), + Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithDefaulter(&testDefaulter{}) + }), + ) - It("should scaffold a custom defaulting webhook which recovers from panics", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom defaulting webhook which recovers from panics", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} - builder.Register(&TestDefaulter{}, &TestDefaulterList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - WithDefaulter(&TestCustomDefaulter{}). - RecoverPanic(true). - // RecoverPanic defaults to true. - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{}) + build(webhookBuilder) + err = webhookBuilder. + RecoverPanic(true). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"", "version":"v1", - "kind":"TestDefaulter" + "kind":"TestDefaulterObject" }, "resource":{ "group":"", @@ -284,58 +300,65 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a mutating webhook path") - path := generateMutatePath(testDefaulterGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) - }) + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a mutating webhook path") + path := generateMutatePath(testDefaulterGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) + }, + Entry("CustomDefaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithCustomDefaulter(&TestCustomDefaulter{}) + }), + Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithDefaulter(&testDefaulter{}) + }), + ) - It("should scaffold a custom validating webhook", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom validating webhook", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestValidator{}, &TestValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + webhook := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhook) + err = webhook.WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { return admission.DefaultLogConstructor(testingLogger, req) }). - WithContextFunc(func(ctx context.Context, request *http.Request) context.Context { - return context.WithValue(ctx, userAgentCtxKey, request.Header.Get(userAgentHeader)) - }). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + WithContextFunc(func(ctx context.Context, request *http.Request) context.Context { + return context.WithValue(ctx, userAgentCtxKey, request.Header.Get(userAgentHeader)) + }). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"foo.test.org", @@ -353,13 +376,13 @@ func runTests(admissionReviewVersion string) { } } }`) - readerWithCxt := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + readerWithCxt := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"foo.test.org", @@ -378,81 +401,88 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a mutating webhook path that doesn't exist") + path := generateMutatePath(testValidatorGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + + By("sending a request to a validating webhook path") + path = generateValidatePath(testValidatorGVK) + _, err = reader.Seek(0, 0) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a mutating webhook path that doesn't exist") - path := generateMutatePath(testValidatorGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - - By("sending a request to a validating webhook path") - path = generateValidatePath(testValidatorGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook with context header validation") - path = generateValidatePath(testValidatorGVK) - _, err = readerWithCxt.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, readerWithCxt) - req.Header.Add("Content-Type", "application/json") - req.Header.Add(userAgentHeader, userAgentValue) - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - }) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":403`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook with context header validation") + path = generateValidatePath(testValidatorGVK) + _, err = readerWithCxt.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, readerWithCxt) + req.Header.Add("Content-Type", "application/json") + req.Header.Add(userAgentHeader, userAgentValue) + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`)) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) - It("should scaffold a custom validating webhook with a custom path", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom validating webhook with a custom path", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestValidator{}, &TestValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - customPath := "/custom-validating-path" - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + customPath := "/custom-validating-path" + webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhookBuilder) + err = webhookBuilder.WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { return admission.DefaultLogConstructor(testingLogger, req) }). - WithValidatorCustomPath(customPath). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + WithValidatorCustomPath(customPath). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"foo.test.org", @@ -471,64 +501,71 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } - By("sending a request to a valiting webhook path that have been overriten by a custom path") - path, err := generateCustomPath(customPath) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook path") - path = generateValidatePath(testValidatorGVK) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - }) + By("sending a request to a valiting webhook path that have been overriten by a custom path") + path, err := generateCustomPath(customPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":403`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook path") + path = generateValidatePath(testValidatorGVK) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) - It("should scaffold a custom validating webhook which recovers from panics", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom validating webhook which recovers from panics", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestValidator{}, &TestValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - RecoverPanic(true). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhookBuilder) + err = webhookBuilder.RecoverPanic(true). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"", @@ -544,56 +581,63 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } - By("sending a request to a validating webhook path") - path := generateValidatePath(testValidatorGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) - }) + By("sending a request to a validating webhook path") + path := generateValidatePath(testValidatorGVK) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) - It("should scaffold a custom validating webhook to validate deletes", func(specCtx SpecContext) { - By("creating a controller manager") - ctx, cancel := context.WithCancel(specCtx) + DescribeTable("should scaffold a custom validating webhook to validate deletes", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + ctx, cancel := context.WithCancel(specCtx) - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestValidator{}, &TestValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhookBuilder) + err = webhookBuilder.Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"", @@ -609,30 +653,30 @@ func runTests(admissionReviewVersion string) { } }`) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a validating webhook path to check for failed delete") - path := generateValidatePath(testValidatorGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - - reader = strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a validating webhook path to check for failed delete") + path := generateValidatePath(testValidatorGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) + + reader = strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"", @@ -647,60 +691,49 @@ func runTests(admissionReviewVersion string) { } } }`) - By("sending a request to a validating webhook path with correct request") - path = generateValidatePath(testValidatorGVK) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - }) - - It("should send an error when trying to register a webhook with more than one For", func() { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaultValidatorGVK.GroupVersion()} - builder.Register(&TestDefaulter{}, &TestDefaulterList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - For(&TestDefaulter{}). - Complete() - Expect(err).To(HaveOccurred()) - }) + By("sending a request to a validating webhook path with correct request") + path = generateValidatePath(testValidatorGVK) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) - It("should scaffold a custom defaulting and validating webhook", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom defaulting and validating webhook", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaultValidator])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaultValidator{}). - WithDefaulter(&TestCustomDefaultValidator{}). - WithValidator(&TestCustomDefaultValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestDefaultValidator{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ @@ -725,69 +758,86 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a mutating webhook path") + path := generateMutatePath(testDefaultValidatorGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook path") + path = generateValidatePath(testDefaultValidatorGVK) + _, err = reader.Seek(0, 0) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a mutating webhook path") - path := generateMutatePath(testDefaultValidatorGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook path") - path = generateValidatePath(testDefaultValidatorGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - }) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + }, + Entry("CustomDefaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithCustomDefaulter(&TestCustomDefaultValidator{}) + b.WithCustomValidator(&TestCustomDefaultValidator{}) + }), + Entry("CustomDefaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithCustomDefaulter(&TestCustomDefaultValidator{}) + b.WithValidator(&testDefaultValidatorValidator{}) + }), + Entry("Defaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithDefaulter(&testValidatorDefaulter{}) + b.WithCustomValidator(&TestCustomDefaultValidator{}) + }), + Entry("Defaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithDefaulter(&testValidatorDefaulter{}) + b.WithValidator(&testDefaultValidatorValidator{}) + }), + ) - It("should scaffold a custom defaulting and validating webhook with a custom path for each of them", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom defaulting and validating webhook with a custom path for each of them", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaultValidator])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - validatingCustomPath := "/custom-validating-path" - defaultingCustomPath := "/custom-defaulting-path" - err = WebhookManagedBy(m). - For(&TestDefaultValidator{}). - WithDefaulter(&TestCustomDefaultValidator{}). - WithValidator(&TestCustomDefaultValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - WithValidatorCustomPath(validatingCustomPath). - WithDefaulterCustomPath(defaultingCustomPath). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + validatingCustomPath := "/custom-validating-path" + defaultingCustomPath := "/custom-defaulting-path" + webhookBuilder := WebhookManagedBy(m, &TestDefaultValidator{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + WithValidatorCustomPath(validatingCustomPath). + WithDefaulterCustomPath(defaultingCustomPath). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ @@ -812,60 +862,77 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } - By("sending a request to a mutating webhook path that have been overriten by the custom path") - path, err := generateCustomPath(defaultingCustomPath) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a mutating webhook path") - path = generateMutatePath(testDefaultValidatorGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - - By("sending a request to a valiting webhook path that have been overriten by a custom path") - path, err = generateCustomPath(validatingCustomPath) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook path") - path = generateValidatePath(testValidatorGVK) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - }) + By("sending a request to a mutating webhook path that have been overriten by the custom path") + path, err := generateCustomPath(defaultingCustomPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a mutating webhook path") + path = generateMutatePath(testDefaultValidatorGVK) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + + By("sending a request to a valiting webhook path that have been overriten by a custom path") + path, err = generateCustomPath(validatingCustomPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook path") + path = generateValidatePath(testValidatorGVK) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + }, + Entry("CustomDefaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithCustomDefaulter(&TestCustomDefaultValidator{}) + b.WithCustomValidator(&TestCustomDefaultValidator{}) + }), + Entry("CustomDefaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithCustomDefaulter(&TestCustomDefaultValidator{}) + b.WithValidator(&testDefaultValidatorValidator{}) + }), + Entry("Defaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithDefaulter(&testValidatorDefaulter{}) + b.WithCustomValidator(&TestCustomDefaultValidator{}) + }), + Entry("Defaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithDefaulter(&testValidatorDefaulter{}) + b.WithValidator(&testDefaultValidatorValidator{}) + }), + ) It("should not scaffold a custom defaulting and a custom validating webhook with the same custom path", func() { By("creating a controller manager") @@ -878,10 +945,9 @@ func runTests(admissionReviewVersion string) { err = builder.AddToScheme(m.GetScheme()) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaultValidator{}). - WithDefaulter(&TestCustomDefaultValidator{}). - WithValidator(&TestCustomDefaultValidator{}). + err = WebhookManagedBy(m, &TestDefaultValidator{}). + WithCustomDefaulter(&TestCustomDefaultValidator{}). + WithCustomValidator(&TestCustomDefaultValidator{}). WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { return admission.DefaultLogConstructor(testingLogger, req) }). @@ -890,77 +956,126 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, err).To(HaveOccurred()) }) - It("should not scaffold a custom defaulting when setting a custom path and a defaulting custom path", func() { - By("creating a controller manager") + DescribeTable("should not scaffold a custom defaulting when setting a custom path and a defaulting custom path", + func(build func(*WebhookBuilder[*TestDefaulterObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + WithDefaulterCustomPath(customPath). + WithCustomPath(customPath). + Complete() + ExpectWithOffset(1, err).To(HaveOccurred()) + }, + Entry("CustomDefaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithCustomDefaulter(&TestCustomDefaulter{}) + }), + Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithDefaulter(&testDefaulter{}) + }), + ) + + DescribeTable("should not scaffold a custom validating when setting a custom path and a validating custom path", + func(build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + WithValidatorCustomPath(customPath). + WithCustomPath(customPath). + Complete() + ExpectWithOffset(1, err).To(HaveOccurred()) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) + + It("should error if both a defaulter and a custom defaulter are set", func() { m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) err = builder.AddToScheme(m.GetScheme()) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - WithDefaulter(&TestCustomDefaulter{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - WithDefaulterCustomPath(customPath). - WithCustomPath(customPath). + err = WebhookManagedBy(m, &TestDefaulterObject{}). + WithDefaulter(&testDefaulter{}). + WithCustomDefaulter(&TestCustomDefaulter{}). Complete() ExpectWithOffset(1, err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("only one of Defaulter or CustomDefaulter can be set")) }) - - It("should not scaffold a custom defaulting when setting a custom path and a validating custom path", func() { - By("creating a controller manager") + It("should error if both a validator and a custom validator are set", func() { m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) err = builder.AddToScheme(m.GetScheme()) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - WithDefaulterCustomPath(customPath). - WithCustomPath(customPath). + err = WebhookManagedBy(m, &TestValidatorObject{}). + WithValidator(&testValidator{}). + WithCustomValidator(&TestCustomValidator{}). Complete() ExpectWithOffset(1, err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("only one of Validator or CustomValidator can be set")) }) } // TestDefaulter. -var _ runtime.Object = &TestDefaulter{} +var _ runtime.Object = &TestDefaulterObject{} -const testDefaulterKind = "TestDefaulter" +const testDefaulterKind = "TestDefaulterObject" -type TestDefaulter struct { +type TestDefaulterObject struct { Replica int `json:"replica,omitempty"` Panic bool `json:"panic,omitempty"` } var testDefaulterGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testDefaulterKind} -func (d *TestDefaulter) GetObjectKind() schema.ObjectKind { return d } -func (d *TestDefaulter) DeepCopyObject() runtime.Object { - return &TestDefaulter{ +func (d *TestDefaulterObject) GetObjectKind() schema.ObjectKind { return d } +func (d *TestDefaulterObject) DeepCopyObject() runtime.Object { + return &TestDefaulterObject{ Replica: d.Replica, } } -func (d *TestDefaulter) GroupVersionKind() schema.GroupVersionKind { +func (d *TestDefaulterObject) GroupVersionKind() schema.GroupVersionKind { return testDefaulterGVK } -func (d *TestDefaulter) SetGroupVersionKind(gvk schema.GroupVersionKind) {} +func (d *TestDefaulterObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {} var _ runtime.Object = &TestDefaulterList{} @@ -970,29 +1085,29 @@ func (*TestDefaulterList) GetObjectKind() schema.ObjectKind { return nil } func (*TestDefaulterList) DeepCopyObject() runtime.Object { return nil } // TestValidator. -var _ runtime.Object = &TestValidator{} +var _ runtime.Object = &TestValidatorObject{} -const testValidatorKind = "TestValidator" +const testValidatorKind = "TestValidatorObject" -type TestValidator struct { +type TestValidatorObject struct { Replica int `json:"replica,omitempty"` Panic bool `json:"panic,omitempty"` } var testValidatorGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testValidatorKind} -func (v *TestValidator) GetObjectKind() schema.ObjectKind { return v } -func (v *TestValidator) DeepCopyObject() runtime.Object { - return &TestValidator{ +func (v *TestValidatorObject) GetObjectKind() schema.ObjectKind { return v } +func (v *TestValidatorObject) DeepCopyObject() runtime.Object { + return &TestValidatorObject{ Replica: v.Replica, } } -func (v *TestValidator) GroupVersionKind() schema.GroupVersionKind { +func (v *TestValidatorObject) GroupVersionKind() schema.GroupVersionKind { return testValidatorGVK } -func (v *TestValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) {} +func (v *TestValidatorObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {} var _ runtime.Object = &TestValidatorList{} @@ -1035,10 +1150,16 @@ type TestDefaultValidatorList struct{} func (*TestDefaultValidatorList) GetObjectKind() schema.ObjectKind { return nil } func (*TestDefaultValidatorList) DeepCopyObject() runtime.Object { return nil } -// TestCustomDefaulter. type TestCustomDefaulter struct{} func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + d := obj.(*TestDefaulterObject) //nolint:ifshort + return (&testDefaulter{}).Default(ctx, d) +} + +type testDefaulter struct{} + +func (*testDefaulter) Default(ctx context.Context, obj *TestDefaulterObject) error { logf.FromContext(ctx).Info("Defaulting object") req, err := admission.RequestFromContext(ctx) if err != nil { @@ -1048,13 +1169,12 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err return fmt.Errorf("expected Kind TestDefaulter got %q", req.Kind.Kind) } - d := obj.(*TestDefaulter) //nolint:ifshort - if d.Panic { + if obj.Panic { panic("fake panic test") } - if d.Replica < 2 { - d.Replica = 2 + if obj.Replica < 2 { + obj.Replica = 2 } return nil @@ -1062,11 +1182,27 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err var _ admission.CustomDefaulter = &TestCustomDefaulter{} -// TestCustomValidator. - type TestCustomValidator struct{} func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + v := obj.(*TestValidatorObject) //nolint:ifshort + return (&testValidator{}).ValidateCreate(ctx, v) +} + +func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + v := newObj.(*TestValidatorObject) + old := oldObj.(*TestValidatorObject) + return (&testValidator{}).ValidateUpdate(ctx, old, v) +} + +func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + v := obj.(*TestValidatorObject) //nolint:ifshort + return (&testValidator{}).ValidateDelete(ctx, v) +} + +type testValidator struct{} + +func (*testValidator) ValidateCreate(ctx context.Context, obj *TestValidatorObject) (admission.Warnings, error) { logf.FromContext(ctx).Info("Validating object") req, err := admission.RequestFromContext(ctx) if err != nil { @@ -1076,18 +1212,17 @@ func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Obje return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) } - v := obj.(*TestValidator) //nolint:ifshort - if v.Panic { + if obj.Panic { panic("fake panic test") } - if v.Replica < 0 { + if obj.Replica < 0 { return nil, errors.New("number of replica should be greater than or equal to 0") } return nil, nil } -func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { +func (*testValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *TestValidatorObject) (admission.Warnings, error) { logf.FromContext(ctx).Info("Validating object") req, err := admission.RequestFromContext(ctx) if err != nil { @@ -1097,13 +1232,11 @@ func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj r return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) } - v := newObj.(*TestValidator) - old := oldObj.(*TestValidator) - if v.Replica < 0 { + if newObj.Replica < 0 { return nil, errors.New("number of replica should be greater than or equal to 0") } - if v.Replica < old.Replica { - return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", v.Replica, old.Replica) + if newObj.Replica < oldObj.Replica { + return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", newObj.Replica, oldObj.Replica) } userAgent, ok := ctx.Value(userAgentCtxKey).(string) @@ -1114,7 +1247,7 @@ func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj r return nil, nil } -func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (*testValidator) ValidateDelete(ctx context.Context, obj *TestValidatorObject) (admission.Warnings, error) { logf.FromContext(ctx).Info("Validating object") req, err := admission.RequestFromContext(ctx) if err != nil { @@ -1124,8 +1257,7 @@ func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Obje return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) } - v := obj.(*TestValidator) //nolint:ifshort - if v.Replica > 0 { + if obj.Replica > 0 { return nil, errors.New("number of replica should be less than or equal to 0 to delete") } return nil, nil @@ -1214,3 +1346,23 @@ func (*TestCustomDefaultValidator) ValidateDelete(ctx context.Context, obj runti } var _ admission.CustomValidator = &TestCustomValidator{} + +type testValidatorDefaulter struct{} + +func (*testValidatorDefaulter) Default(ctx context.Context, obj *TestDefaultValidator) error { + return (&TestCustomDefaultValidator{}).Default(ctx, obj) +} + +type testDefaultValidatorValidator struct{} + +func (*testDefaultValidatorValidator) ValidateCreate(ctx context.Context, obj *TestDefaultValidator) (admission.Warnings, error) { + return (&TestCustomDefaultValidator{}).ValidateCreate(ctx, obj) +} + +func (*testDefaultValidatorValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *TestDefaultValidator) (admission.Warnings, error) { + return (&TestCustomDefaultValidator{}).ValidateUpdate(ctx, oldObj, newObj) +} + +func (*testDefaultValidatorValidator) ValidateDelete(ctx context.Context, obj *TestDefaultValidator) (admission.Warnings, error) { + return (&TestCustomDefaultValidator{}).ValidateDelete(ctx, obj) +} diff --git a/pkg/webhook/admission/defaulter_custom.go b/pkg/webhook/admission/defaulter_custom.go index a703cbd2c5..1dc8af10ee 100644 --- a/pkg/webhook/admission/defaulter_custom.go +++ b/pkg/webhook/admission/defaulter_custom.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "net/http" + "reflect" "slices" "gomodules.xyz/jsonpatch/v2" @@ -31,11 +32,15 @@ import ( "k8s.io/apimachinery/pkg/util/sets" ) -// CustomDefaulter defines functions for setting defaults on resources. -type CustomDefaulter interface { - Default(ctx context.Context, obj runtime.Object) error +// Defaulter defines functions for setting defaults on resources. +type Defaulter[T runtime.Object] interface { + Default(ctx context.Context, obj T) error } +// CustomDefaulter defines functions for setting defaults on resources. +// Deprecated: CustomDefaulter is deprecated, use Defaulter instead +type CustomDefaulter = Defaulter[runtime.Object] + type defaulterOptions struct { removeUnknownOrOmitableFields bool } @@ -50,6 +55,29 @@ func DefaulterRemoveUnknownOrOmitableFields(o *defaulterOptions) { o.removeUnknownOrOmitableFields = true } +// WithDefaulter creates a new Webhook for a Defaulter interface. +func WithDefaulter[T runtime.Object](scheme *runtime.Scheme, defaulter Defaulter[T], opts ...DefaulterOption) *Webhook { + options := &defaulterOptions{} + for _, o := range opts { + o(options) + } + return &Webhook{ + Handler: &defaulterForType[T]{ + defaulter: defaulter, + decoder: NewDecoder(scheme), + removeUnknownOrOmitableFields: options.removeUnknownOrOmitableFields, + new: func() T { + var zero T + typ := reflect.TypeOf(zero) + if typ.Kind() == reflect.Ptr { + return reflect.New(typ.Elem()).Interface().(T) + } + return zero + }, + }, + } +} + // WithCustomDefaulter creates a new Webhook for a CustomDefaulter interface. func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter CustomDefaulter, opts ...DefaulterOption) *Webhook { options := &defaulterOptions{} @@ -57,33 +85,30 @@ func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter C o(options) } return &Webhook{ - Handler: &defaulterForType{ - object: obj, + Handler: &defaulterForType[runtime.Object]{ defaulter: defaulter, decoder: NewDecoder(scheme), removeUnknownOrOmitableFields: options.removeUnknownOrOmitableFields, + new: func() runtime.Object { return obj.DeepCopyObject() }, }, } } -type defaulterForType struct { - defaulter CustomDefaulter - object runtime.Object +type defaulterForType[T runtime.Object] struct { + defaulter Defaulter[T] decoder Decoder removeUnknownOrOmitableFields bool + new func() T } // Handle handles admission requests. -func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { +func (h *defaulterForType[T]) Handle(ctx context.Context, req Request) Response { if h.decoder == nil { panic("decoder should never be nil") } if h.defaulter == nil { panic("defaulter should never be nil") } - if h.object == nil { - panic("object should never be nil") - } // Always skip when a DELETE operation received in custom mutation handler. if req.Operation == admissionv1.Delete { @@ -98,15 +123,15 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { ctx = NewContextWithRequest(ctx, req) // Get the object in the request - obj := h.object.DeepCopyObject() + obj := h.new() if err := h.decoder.Decode(req, obj); err != nil { return Errored(http.StatusBadRequest, err) } // Keep a copy of the object if needed - var originalObj runtime.Object + var originalObj T if !h.removeUnknownOrOmitableFields { - originalObj = obj.DeepCopyObject() + originalObj = obj.DeepCopyObject().(T) } // Default the object @@ -131,7 +156,7 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { return handlerResponse } -func (h *defaulterForType) dropSchemeRemovals(r Response, original runtime.Object, raw []byte) Response { +func (h *defaulterForType[T]) dropSchemeRemovals(r Response, original T, raw []byte) Response { const opRemove = "remove" if !r.Allowed || r.PatchType == nil { return r diff --git a/pkg/webhook/admission/validator_custom.go b/pkg/webhook/admission/validator_custom.go index ef1be52a8f..abd68e88bf 100644 --- a/pkg/webhook/admission/validator_custom.go +++ b/pkg/webhook/admission/validator_custom.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "net/http" + "reflect" v1 "k8s.io/api/admission/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -30,54 +31,77 @@ import ( // Warnings represents warning messages. type Warnings []string -// CustomValidator defines functions for validating an operation. +// Validator defines functions for validating an operation. // The object to be validated is passed into methods as a parameter. -type CustomValidator interface { +type Validator[T runtime.Object] interface { // ValidateCreate validates the object on creation. // The optional warnings will be added to the response as warning messages. // Return an error if the object is invalid. - ValidateCreate(ctx context.Context, obj runtime.Object) (warnings Warnings, err error) + ValidateCreate(ctx context.Context, obj T) (warnings Warnings, err error) // ValidateUpdate validates the object on update. // The optional warnings will be added to the response as warning messages. // Return an error if the object is invalid. - ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (warnings Warnings, err error) + ValidateUpdate(ctx context.Context, oldObj, newObj T) (warnings Warnings, err error) // ValidateDelete validates the object on deletion. // The optional warnings will be added to the response as warning messages. // Return an error if the object is invalid. - ValidateDelete(ctx context.Context, obj runtime.Object) (warnings Warnings, err error) + ValidateDelete(ctx context.Context, obj T) (warnings Warnings, err error) +} + +// CustomValidator defines functions for validating an operation. +// Deprecated: CustomValidator is deprecated, use Validator instead +type CustomValidator = Validator[runtime.Object] + +// WithValidator creates a new Webhook for validating the provided type. +func WithValidator[T runtime.Object](scheme *runtime.Scheme, validator Validator[T]) *Webhook { + return &Webhook{ + Handler: &validatorForType[T]{ + validator: validator, + decoder: NewDecoder(scheme), + new: func() T { + var zero T + typ := reflect.TypeOf(zero) + if typ.Kind() == reflect.Ptr { + return reflect.New(typ.Elem()).Interface().(T) + } + return zero + }, + }, + } } -// WithCustomValidator creates a new Webhook for validating the provided type. +// WithCustomValidator creates a new Webhook for a CustomValidator. +// Deprecated: WithCustomValidator is deprecated, use WithValidator instead func WithCustomValidator(scheme *runtime.Scheme, obj runtime.Object, validator CustomValidator) *Webhook { return &Webhook{ - Handler: &validatorForType{object: obj, validator: validator, decoder: NewDecoder(scheme)}, + Handler: &validatorForType[runtime.Object]{ + validator: validator, + decoder: NewDecoder(scheme), + new: func() runtime.Object { return obj.DeepCopyObject() }, + }, } } -type validatorForType struct { - validator CustomValidator - object runtime.Object +type validatorForType[T runtime.Object] struct { + validator Validator[T] decoder Decoder + new func() T } // Handle handles admission requests. -func (h *validatorForType) Handle(ctx context.Context, req Request) Response { +func (h *validatorForType[T]) Handle(ctx context.Context, req Request) Response { if h.decoder == nil { panic("decoder should never be nil") } if h.validator == nil { panic("validator should never be nil") } - if h.object == nil { - panic("object should never be nil") - } ctx = NewContextWithRequest(ctx, req) - // Get the object in the request - obj := h.object.DeepCopyObject() + obj := h.new() var err error var warnings []string @@ -93,7 +117,7 @@ func (h *validatorForType) Handle(ctx context.Context, req Request) Response { warnings, err = h.validator.ValidateCreate(ctx, obj) case v1.Update: - oldObj := obj.DeepCopyObject() + oldObj := h.new() if err := h.decoder.DecodeRaw(req.Object, obj); err != nil { return Errored(http.StatusBadRequest, err) } diff --git a/pkg/webhook/alias.go b/pkg/webhook/alias.go index 2882e7bab3..b4f16a3f5f 100644 --- a/pkg/webhook/alias.go +++ b/pkg/webhook/alias.go @@ -24,9 +24,11 @@ import ( // define some aliases for common bits of the webhook functionality // CustomDefaulter defines functions for setting defaults on resources. +// Deprecated: Use admission.Defaulter instead. type CustomDefaulter = admission.CustomDefaulter // CustomValidator defines functions for validating an operation. +// Deprecated: Use admission.Validator instead. type CustomValidator = admission.CustomValidator // AdmissionRequest defines the input for an admission handler. From b17aca2d504b785eb4b70ef46877266f4ae77d2c Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 1 Nov 2025 14:42:16 +0800 Subject: [PATCH 39/68] Add CreateOrPatch function in alias.go This is the same as alias to CreateOrUpdate in alias.go. --- alias.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/alias.go b/alias.go index dde2c5b930..e2ac45a5e0 100644 --- a/alias.go +++ b/alias.go @@ -111,6 +111,14 @@ var ( // including all CRD resources. NewManager = manager.New + // CreateOrPatch creates or patches the given object obj in the Kubernetes + // cluster. The object's desired state should be reconciled with the existing + // state using the passed in ReconcileFn. obj must be a struct pointer so that + // obj can be patched with the content returned by the Server. + // + // It returns the executed operation and an error. + CreateOrPatch = controllerutil.CreateOrPatch + // CreateOrUpdate creates or updates the given object obj in the Kubernetes // cluster. The object's desired state should be reconciled with the existing // state using the passed in ReconcileFn. obj must be a struct pointer so that From 88b888d51d762f77a74c7f4c178583c7ac5f4ffa Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Sat, 1 Nov 2025 14:15:46 +0100 Subject: [PATCH 40/68] cache: Allow fine-granular configuration of SyncPeriod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- pkg/cache/cache.go | 84 +++++++++++++++++++++++++++++++++--- pkg/cache/defaulting_test.go | 24 +++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 107a7f1cda..b814170de1 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -307,6 +307,42 @@ type ByObject struct { // // Defaults to true. EnableWatchBookmarks *bool + + // SyncPeriod determines the minimum frequency at which watched resources are + // reconciled. A lower period will correct entropy more quickly, but reduce + // responsiveness to change if there are many watched resources. Change this + // value only if you know what you are doing. Defaults to 10 hours if unset. + // there will a 10 percent jitter between the SyncPeriod of all controllers + // so that all controllers will not send list requests simultaneously. + // + // This applies to all controllers. + // + // A period sync happens for two reasons: + // 1. To insure against a bug in the controller that causes an object to not + // be requeued, when it otherwise should be requeued. + // 2. To insure against an unknown bug in controller-runtime, or its dependencies, + // that causes an object to not be requeued, when it otherwise should be + // requeued, or to be removed from the queue, when it otherwise should not + // be removed. + // + // If you want + // 1. to insure against missed watch events, or + // 2. to poll services that cannot be watched, + // then we recommend that, instead of changing the default period, the + // controller requeue, with a constant duration `t`, whenever the controller + // 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 locally trigger an artificial Update event with the same + // object in both ObjectOld and ObjectNew for everything that is in the + // cache. + // + // Predicates or Handlers that expect ObjectOld and ObjectNew to be different + // (such as GenerationChangedPredicate) will filter out this event, preventing + // it from triggering a reconciliation. + // SyncPeriod does not sync between the local cache and the server. + SyncPeriod *time.Duration } // Config describes all potential options for a given watch. @@ -342,6 +378,42 @@ type Config struct { // // Defaults to true. EnableWatchBookmarks *bool + + // SyncPeriod determines the minimum frequency at which watched resources are + // reconciled. A lower period will correct entropy more quickly, but reduce + // responsiveness to change if there are many watched resources. Change this + // value only if you know what you are doing. Defaults to 10 hours if unset. + // there will a 10 percent jitter between the SyncPeriod of all controllers + // so that all controllers will not send list requests simultaneously. + // + // This applies to all controllers. + // + // A period sync happens for two reasons: + // 1. To insure against a bug in the controller that causes an object to not + // be requeued, when it otherwise should be requeued. + // 2. To insure against an unknown bug in controller-runtime, or its dependencies, + // that causes an object to not be requeued, when it otherwise should be + // requeued, or to be removed from the queue, when it otherwise should not + // be removed. + // + // If you want + // 1. to insure against missed watch events, or + // 2. to poll services that cannot be watched, + // then we recommend that, instead of changing the default period, the + // controller requeue, with a constant duration `t`, whenever the controller + // 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 locally trigger an artificial Update event with the same + // object in both ObjectOld and ObjectNew for everything that is in the + // cache. + // + // Predicates or Handlers that expect ObjectOld and ObjectNew to be different + // (such as GenerationChangedPredicate) will filter out this event, preventing + // it from triggering a reconciliation. + // SyncPeriod does not sync between the local cache and the server. + SyncPeriod *time.Duration } // NewCacheFunc - Function for creating a new cache from the options and a rest config. @@ -412,6 +484,7 @@ func optionDefaultsToConfig(opts *Options) Config { Transform: opts.DefaultTransform, UnsafeDisableDeepCopy: opts.DefaultUnsafeDisableDeepCopy, EnableWatchBookmarks: opts.DefaultEnableWatchBookmarks, + SyncPeriod: opts.SyncPeriod, } } @@ -422,6 +495,7 @@ func byObjectToConfig(byObject ByObject) Config { Transform: byObject.Transform, UnsafeDisableDeepCopy: byObject.UnsafeDisableDeepCopy, EnableWatchBookmarks: byObject.EnableWatchBookmarks, + SyncPeriod: byObject.SyncPeriod, } } @@ -435,7 +509,7 @@ func newCache(restConfig *rest.Config, opts Options) newCacheFunc { HTTPClient: opts.HTTPClient, Scheme: opts.Scheme, Mapper: opts.Mapper, - ResyncPeriod: *opts.SyncPeriod, + ResyncPeriod: ptr.Deref(config.SyncPeriod, defaultSyncPeriod), Namespace: namespace, Selector: internal.Selector{ Label: config.LabelSelector, @@ -533,6 +607,7 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { byObject.Transform = defaultedConfig.Transform byObject.UnsafeDisableDeepCopy = defaultedConfig.UnsafeDisableDeepCopy byObject.EnableWatchBookmarks = defaultedConfig.EnableWatchBookmarks + byObject.SyncPeriod = defaultedConfig.SyncPeriod } opts.ByObject[obj] = byObject @@ -554,10 +629,6 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) { opts.DefaultNamespaces[namespace] = cfg } - // Default the resync period to 10 hours if unset - if opts.SyncPeriod == nil { - opts.SyncPeriod = &defaultSyncPeriod - } return opts, nil } @@ -577,6 +648,9 @@ func defaultConfig(toDefault, defaultFrom Config) Config { if toDefault.EnableWatchBookmarks == nil { toDefault.EnableWatchBookmarks = defaultFrom.EnableWatchBookmarks } + if toDefault.SyncPeriod == nil { + toDefault.SyncPeriod = defaultFrom.SyncPeriod + } return toDefault } diff --git a/pkg/cache/defaulting_test.go b/pkg/cache/defaulting_test.go index d9d0dcceb3..89a0334324 100644 --- a/pkg/cache/defaulting_test.go +++ b/pkg/cache/defaulting_test.go @@ -249,6 +249,30 @@ func TestDefaultOpts(t *testing.T) { return cmp.Diff(expected, o.ByObject[pod].EnableWatchBookmarks) }, }, + { + name: "ByObject.SyncPeriod gets defaulted from SyncPeriod", + in: Options{ + ByObject: map[client.Object]ByObject{pod: {}}, + SyncPeriod: ptr.To(5 * time.Minute), + }, + + verification: func(o Options) string { + expected := ptr.To(5 * time.Minute) + return cmp.Diff(expected, o.ByObject[pod].SyncPeriod) + }, + }, + { + name: "ByObject.SyncPeriod doesn't get defaulted when set", + in: Options{ + ByObject: map[client.Object]ByObject{pod: {SyncPeriod: ptr.To(1 * time.Minute)}}, + SyncPeriod: ptr.To(5 * time.Minute), + }, + + verification: func(o Options) string { + expected := ptr.To(1 * time.Minute) + return cmp.Diff(expected, o.ByObject[pod].SyncPeriod) + }, + }, { name: "DefaultNamespace label selector gets defaulted from DefaultLabelSelector", in: Options{ From d2a3b612d7b290c8836bcd2562af0e1ea4ce3d99 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Tue, 4 Nov 2025 10:39:09 -0600 Subject: [PATCH 41/68] setup-envtest: select the newest Kubernetes by default The order inadvertently changed in 0ddbc5205c01f30a1bc604e9e4857a22d6b57243. Fixes: kubernetes-sigs/controller-runtime#3379 Signed-off-by: Chris Bandy --- tools/setup-envtest/remote/http_client.go | 2 +- tools/setup-envtest/store/store.go | 3 ++- tools/setup-envtest/versions/misc_test.go | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/tools/setup-envtest/remote/http_client.go b/tools/setup-envtest/remote/http_client.go index a87ef1f105..7cbe3ee1d4 100644 --- a/tools/setup-envtest/remote/http_client.go +++ b/tools/setup-envtest/remote/http_client.go @@ -88,7 +88,7 @@ func (c *HTTPClient) ListVersions(ctx context.Context) ([]versions.Set, error) { } // sort in inverse order so that the newest one is first slices.SortStableFunc(res, func(i, j versions.Set) int { - return i.Version.Compare(j.Version) + return j.Version.Compare(i.Version) }) return res, nil diff --git a/tools/setup-envtest/store/store.go b/tools/setup-envtest/store/store.go index 1e1d4beb3c..1e8053e0cc 100644 --- a/tools/setup-envtest/store/store.go +++ b/tools/setup-envtest/store/store.go @@ -105,7 +105,8 @@ func (s *Store) List(ctx context.Context, matching Filter) ([]Item, error) { slices.SortStableFunc(res, func(i, j Item) int { if !i.Version.Matches(j.Version) { - return i.Version.Compare(j.Version) + // sort in inverse order so that the newest one is first + return j.Version.Compare(i.Version) } return orderPlatforms(i.Platform, j.Platform) }) diff --git a/tools/setup-envtest/versions/misc_test.go b/tools/setup-envtest/versions/misc_test.go index 1c0dbb68db..187218f82e 100644 --- a/tools/setup-envtest/versions/misc_test.go +++ b/tools/setup-envtest/versions/misc_test.go @@ -17,6 +17,9 @@ limitations under the License. package versions_test import ( + "math/rand/v2" + "slices" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -44,6 +47,23 @@ var _ = Describe("Concrete", func() { Specify("newer major should be newer", func() { Expect(ver1163.Compare(Concrete{Major: 0, Minor: 16, Patch: 3})).To(Equal(1)) }) + + Describe("sorting", func() { + ver16 := Concrete{Major: 1, Minor: 16} + ver17 := Concrete{Major: 1, Minor: 17} + many := []Concrete{ver16, ver17, ver1163} + + BeforeEach(func() { + rand.Shuffle(len(many), func(i, j int) { + many[i], many[j] = many[j], many[i] + }) + }) + + Specify("newer versions are later", func() { + slices.SortStableFunc(many, Concrete.Compare) + Expect(many).To(Equal([]Concrete{ver16, ver1163, ver17})) + }) + }) }) }) From 528ec24c5e85b9d17674f1ad266fdbd9c15f0b35 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Wed, 5 Nov 2025 16:44:35 -0600 Subject: [PATCH 42/68] Add setup-envtest tests to test-all.sh These tests are passing again. --- Makefile | 6 +----- hack/test-all.sh | 1 + 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 5dded97481..86a5eddf80 100644 --- a/Makefile +++ b/Makefile @@ -69,13 +69,9 @@ help: ## Display this help ## -------------------------------------- .PHONY: test -test: test-tools ## Run the script check-everything.sh which will check all. +test: ## Run the script check-everything.sh which will check all. TRACE=1 ./hack/check-everything.sh -.PHONY: test-tools -test-tools: ## tests the tools codebase (setup-envtest) - cd tools/setup-envtest && go test ./... - ## -------------------------------------- ## Binaries ## -------------------------------------- diff --git a/hack/test-all.sh b/hack/test-all.sh index 34d841cfd0..9363fa5963 100755 --- a/hack/test-all.sh +++ b/hack/test-all.sh @@ -26,6 +26,7 @@ fi result=0 go test -v -race ${P_FLAG} ${MOD_OPT} ./... --ginkgo.fail-fast ${GINKGO_ARGS} || result=$? +(cd tools/setup-envtest && go test -v -race ${P_FLAG} ${MOD_OPT} ./... --ginkgo.fail-fast ${GINKGO_ARGS}) || result=$? if [[ -n ${ARTIFACTS:-} ]]; then mkdir -p ${ARTIFACTS} From 93b414585332499590c3e7632cac6a37b6e75a34 Mon Sep 17 00:00:00 2001 From: godwinpang Date: Wed, 5 Nov 2025 15:58:19 -0800 Subject: [PATCH 43/68] =?UTF-8?q?=E2=9C=A8=20Add=20ReconcileTimeouts=20met?= =?UTF-8?q?ric=20to=20track=20ReconciliationTimeout=20timeouts.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a new metric controller_runtime_reconcile_timeouts_total to track when a controller's reconciliation has reached a ReconciliationTimeout. This provides visibility into when reconcile operations time out due to the controller-runtime wrapper timeout, allowing users to alert / monitor unexpectedly long running controller reconiliations. --- pkg/internal/controller/controller.go | 22 +++- pkg/internal/controller/controller_test.go | 122 +++++++++++++++++++++ pkg/internal/controller/metrics/metrics.go | 10 ++ 3 files changed, 152 insertions(+), 2 deletions(-) diff --git a/pkg/internal/controller/controller.go b/pkg/internal/controller/controller.go index 7dd06957eb..f2638b9d9b 100644 --- a/pkg/internal/controller/controller.go +++ b/pkg/internal/controller/controller.go @@ -39,6 +39,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" ) +// errReconciliationTimeout is the error used as the cause when the ReconciliationTimeout guardrail fires. +// This allows us to distinguish wrapper timeouts from user-initiated context cancellations. +var errReconciliationTimeout = errors.New("reconciliation timeout") + // Options are the arguments for creating a new Controller. type Options[request comparable] struct { // Reconciler is a function that can be called at any time with the Name / Namespace of an object and @@ -207,13 +211,26 @@ func (c *Controller[request]) Reconcile(ctx context.Context, req request) (_ rec } }() + var timeoutCause error if c.ReconciliationTimeout > 0 { + timeoutCause = errReconciliationTimeout var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, c.ReconciliationTimeout) + ctx, cancel = context.WithTimeoutCause(ctx, c.ReconciliationTimeout, timeoutCause) defer cancel() } - return c.Do.Reconcile(ctx, req) + res, err := c.Do.Reconcile(ctx, req) + + // Check if the reconciliation timed out due to our wrapper timeout guardrail. + // We check ctx.Err() == context.DeadlineExceeded first to ensure the context was actually + // cancelled due to a deadline (not parent cancellation or other reasons), then verify it was + // our specific timeout cause. This prevents false positives from parent context cancellations + // or other timeout scenarios. + if timeoutCause != nil && ctx.Err() == context.DeadlineExceeded && errors.Is(context.Cause(ctx), timeoutCause) { + ctrlmetrics.ReconcileTimeouts.WithLabelValues(c.Name).Inc() + } + + return res, err } // Watch implements controller.Controller. @@ -437,6 +454,7 @@ func (c *Controller[request]) initMetrics() { ctrlmetrics.ReconcileErrors.WithLabelValues(c.Name).Add(0) ctrlmetrics.TerminalReconcileErrors.WithLabelValues(c.Name).Add(0) ctrlmetrics.ReconcilePanics.WithLabelValues(c.Name).Add(0) + ctrlmetrics.ReconcileTimeouts.WithLabelValues(c.Name).Add(0) ctrlmetrics.WorkerCount.WithLabelValues(c.Name).Set(float64(c.MaxConcurrentReconciles)) ctrlmetrics.ActiveWorkers.WithLabelValues(c.Name).Set(0) } diff --git a/pkg/internal/controller/controller_test.go b/pkg/internal/controller/controller_test.go index 6d62b80e22..259f6669e2 100644 --- a/pkg/internal/controller/controller_test.go +++ b/pkg/internal/controller/controller_test.go @@ -155,6 +155,128 @@ var _ = Describe("controller", func() { Expect(err).To(Equal(context.DeadlineExceeded)) }) + Context("prometheus metric reconcile_timeouts", func() { + var reconcileTimeouts dto.Metric + + BeforeEach(func() { + ctrlmetrics.ReconcileTimeouts.Reset() + reconcileTimeouts.Reset() + ctrl.Name = testControllerName + Expect(ctrlmetrics.ReconcileTimeouts.WithLabelValues(ctrl.Name).Write(&reconcileTimeouts)).To(Succeed()) + Expect(reconcileTimeouts.GetCounter().GetValue()).To(Equal(0.0)) + }) + + It("should increment when ReconciliationTimeout context timeout hits DeadlineExceeded", func(ctx SpecContext) { + ctrl.ReconciliationTimeout = time.Duration(1) + ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { + <-ctx.Done() + return reconcile.Result{}, ctx.Err() + }) + _, err := ctrl.Reconcile(ctx, + reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}) + Expect(err).To(HaveOccurred()) + Expect(err).To(Equal(context.DeadlineExceeded)) + + Expect(ctrlmetrics.ReconcileTimeouts.WithLabelValues(ctrl.Name).Write(&reconcileTimeouts)).To(Succeed()) + Expect(reconcileTimeouts.GetCounter().GetValue()).To(Equal(1.0)) + }) + + It("should not increment when user code cancels context earlier than the ReconciliationTimeout", func(ctx SpecContext) { + ctrl.ReconciliationTimeout = 10 * time.Second + userCancelCause := errors.New("user cancellation") + ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { + // User code creates its own timeout with a different cause + userCtx, cancel := context.WithTimeoutCause(ctx, time.Millisecond, userCancelCause) + defer cancel() + <-userCtx.Done() + return reconcile.Result{}, context.Cause(userCtx) + }) + _, err := ctrl.Reconcile(ctx, + reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, userCancelCause)).To(BeTrue()) + + // Metric should not be incremented because the context timeout didn't hit DeadlineExceeded + Expect(ctrlmetrics.ReconcileTimeouts.WithLabelValues(ctrl.Name).Write(&reconcileTimeouts)).To(Succeed()) + Expect(reconcileTimeouts.GetCounter().GetValue()).To(Equal(0.0)) + }) + + It("should not increment when reconciliation completes before timeout", func(ctx SpecContext) { + ctrl.ReconciliationTimeout = 10 * time.Second + ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { + // Reconcile completes successfully before timeout + return reconcile.Result{}, nil + }) + _, err := ctrl.Reconcile(ctx, + reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}) + Expect(err).NotTo(HaveOccurred()) + + // Metric should not be incremented because the timeout was not exceeded + Expect(ctrlmetrics.ReconcileTimeouts.WithLabelValues(ctrl.Name).Write(&reconcileTimeouts)).To(Succeed()) + Expect(reconcileTimeouts.GetCounter().GetValue()).To(Equal(0.0)) + }) + + It("should increment multiple times when multiple reconciles timeout", func(ctx SpecContext) { + ctrl.ReconciliationTimeout = time.Duration(1) + ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { + <-ctx.Done() + return reconcile.Result{}, ctx.Err() + }) + + const numTimeouts = 3 + // Call Reconcile multiple times, each should timeout and increment the metric + for i := range numTimeouts { + _, err := ctrl.Reconcile(ctx, + reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: fmt.Sprintf("bar%d", i)}}) + Expect(err).To(HaveOccurred()) + Expect(err).To(Equal(context.DeadlineExceeded)) + } + + // Metric should be incremented 3 times + Expect(ctrlmetrics.ReconcileTimeouts.WithLabelValues(ctrl.Name).Write(&reconcileTimeouts)).To(Succeed()) + Expect(reconcileTimeouts.GetCounter().GetValue()).To(Equal(float64(numTimeouts))) + }) + + It("should not increment when parent context is cancelled", func(ctx SpecContext) { + parentCtx, cancel := context.WithCancel(ctx) + defer cancel() + + ctrl.ReconciliationTimeout = 10 * time.Second + ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { + // Wait for parent cancellation + <-ctx.Done() + return reconcile.Result{}, ctx.Err() + }) + + // Cancel parent context immediately + cancel() + + _, err := ctrl.Reconcile(parentCtx, + reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}) + Expect(err).To(HaveOccurred()) + Expect(err).To(Equal(context.Canceled)) + + // Metric should not be incremented because the wrapper timeout didn't fire + Expect(ctrlmetrics.ReconcileTimeouts.WithLabelValues(ctrl.Name).Write(&reconcileTimeouts)).To(Succeed()) + Expect(reconcileTimeouts.GetCounter().GetValue()).To(Equal(0.0)) + }) + + It("should not increment when ReconciliationTimeout is zero", func(ctx SpecContext) { + // Ensure ReconciliationTimeout is zero (not set) + ctrl.ReconciliationTimeout = 0 + ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { + return reconcile.Result{}, nil + }) + _, err := ctrl.Reconcile(ctx, + reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}) + Expect(err).NotTo(HaveOccurred()) + + // Metric should not be incremented because ReconciliationTimeout is not set + Expect(ctrlmetrics.ReconcileTimeouts.WithLabelValues(ctrl.Name).Write(&reconcileTimeouts)).To(Succeed()) + Expect(reconcileTimeouts.GetCounter().GetValue()).To(Equal(0.0)) + }) + }) + It("should not configure a timeout if ReconciliationTimeout is zero", func(ctx SpecContext) { ctrl.Do = reconcile.Func(func(ctx context.Context, _ reconcile.Request) (reconcile.Result, error) { defer GinkgoRecover() diff --git a/pkg/internal/controller/metrics/metrics.go b/pkg/internal/controller/metrics/metrics.go index 450e9ae25b..39b435c453 100644 --- a/pkg/internal/controller/metrics/metrics.go +++ b/pkg/internal/controller/metrics/metrics.go @@ -80,6 +80,15 @@ var ( Name: "controller_runtime_active_workers", Help: "Number of currently used workers per controller", }, []string{"controller"}) + + // ReconcileTimeouts is a prometheus counter metric which holds the total + // number of reconciliations that timed out due to the ReconciliationTimeout + // context timeout. This metric only increments when the wrapper timeout fires, + // not when user reconcilers cancels the context or completes before the timeout. + ReconcileTimeouts = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "controller_runtime_reconcile_timeouts_total", + Help: "Total number of reconciliation timeouts per controller", + }, []string{"controller"}) ) func init() { @@ -91,6 +100,7 @@ func init() { ReconcileTime, WorkerCount, ActiveWorkers, + ReconcileTimeouts, // expose process metrics like CPU, Memory, file descriptor usage etc. collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), // expose all Go runtime metrics like GC stats, memory stats etc. From cfcc8d4d60035f192afbc8e51cf2579bfea2252f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:03:34 +0000 Subject: [PATCH 44/68] :seedling: Bump the all-github-actions group with 2 updates Bumps the all-github-actions group with 2 updates: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `golangci/golangci-lint-action` from 8.0.0 to 9.0.0 - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/4afd733a84b1f43292c63897423277bb7f4313a9...0a35821d5c230e903fcfe077583637dea1b27b47) Updates `softprops/action-gh-release` from 2.4.1 to 2.4.2 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/6da8fa9354ddfdc4aeace5fc48d7f679b5214090...5be0e66d93ac7ed76da52eca8bb058f665c3a5fe) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-github-actions - dependency-name: softprops/action-gh-release dependency-version: 2.4.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 2bbbb33aea..65e849da2d 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -32,7 +32,7 @@ jobs: with: go-version: ${{ steps.vars.outputs.go_version }} - name: golangci-lint - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # tag=v8.0.0 + uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # tag=v9.0.0 with: version: v2.5.0 args: --output.text.print-linter-name=true --output.text.colors=true --timeout 10m diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d9b9f394ef..ce3d7ad462 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,7 +29,7 @@ jobs: run: | make release - name: Release - uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # tag=v2.4.1 + uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # tag=v2.4.2 with: draft: false files: tools/setup-envtest/out/* From 81bad0719862fb390e7ba4d7d52f2c439f5d2e80 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sun, 16 Nov 2025 09:16:17 -0500 Subject: [PATCH 45/68] :seedling: Bump k8s.io/* deps to v0.35.0-alpha.3 --- examples/scratch-env/go.mod | 21 +++--- examples/scratch-env/go.sum | 91 ++++++++++---------------- go.mod | 34 +++++----- go.sum | 123 +++++++++++++++++------------------- tools/setup-envtest/go.mod | 21 +++--- tools/setup-envtest/go.sum | 69 +++++++++++++++----- 6 files changed, 181 insertions(+), 178 deletions(-) diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod index 06b99d7b0d..19b7aec9e6 100644 --- a/examples/scratch-env/go.mod +++ b/examples/scratch-env/go.mod @@ -21,7 +21,6 @@ require ( 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/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -39,27 +38,27 @@ require ( github.com/prometheus/procfs v0.16.1 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.9.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.0-alpha.1 // indirect - k8s.io/apiextensions-apiserver v0.35.0-alpha.1 // indirect - k8s.io/apimachinery v0.35.0-alpha.1 // indirect - k8s.io/client-go v0.35.0-alpha.1 // indirect + k8s.io/api v0.35.0-alpha.3 // indirect + k8s.io/apiextensions-apiserver v0.35.0-alpha.3 // indirect + k8s.io/apimachinery v0.35.0-alpha.3 // indirect + k8s.io/client-go v0.35.0-alpha.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum index a1bac915e3..e99c71699a 100644 --- a/examples/scratch-env/go.sum +++ b/examples/scratch-env/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -31,8 +33,6 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 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/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -42,16 +42,14 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -73,10 +71,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -89,8 +87,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -106,59 +104,34 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= @@ -173,20 +146,20 @@ 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.35.0-alpha.1 h1:aL5Q6ZV4MQ2NZMmlnAsV7wj9a30gLhlLnGbx6GUmuBs= -k8s.io/api v0.35.0-alpha.1/go.mod h1:BoZqpN+rs1nX+WI4b+iOCpHIAZT1A5Cx29nfk4Kn4DY= -k8s.io/apiextensions-apiserver v0.35.0-alpha.1 h1:x/nDc4Ic4j9Pjn8trEuRIkbLgVWkSPTNkDWrNGUnCtg= -k8s.io/apiextensions-apiserver v0.35.0-alpha.1/go.mod h1:g00cZRV928nCiZtLlyedrVInFkJJHxzy8QWCyYJslWQ= -k8s.io/apimachinery v0.35.0-alpha.1 h1:FZCO78xXJf7Bb7oLzw5p6nakz/SWaGTi4+IaOl7uAYk= -k8s.io/apimachinery v0.35.0-alpha.1/go.mod h1:1YSL0XujdSTcnuHOR73D16EdW+d49JOdd8TXjCo6Dhc= -k8s.io/client-go v0.35.0-alpha.1 h1:DbQuaoETvFkhWfIckZj3hj1iNnBvEIdiWjSlosmtlX4= -k8s.io/client-go v0.35.0-alpha.1/go.mod h1:CI5Ggq6AukXNEBV2UeBgY4tfrOZfDSa7KuoWwLfHqGA= +k8s.io/api v0.35.0-alpha.3 h1:BdcXkJ4n/NKhfg06PaSDG8r8Mpe9g3KO9Fkj7B/F8/4= +k8s.io/api v0.35.0-alpha.3/go.mod h1:SArWbUwVv7VhTGGbKX0RoMPXiT6ztjjzkKpRRdl6+E0= +k8s.io/apiextensions-apiserver v0.35.0-alpha.3 h1:Js9dTA0LVvR93tWOZtyJuZKMs0CQuaxNiaM1dwtUM0Q= +k8s.io/apiextensions-apiserver v0.35.0-alpha.3/go.mod h1:LWHywtk0D0qSiJd4Ql65tMY8hDKJYsgBg2jQijeZJNE= +k8s.io/apimachinery v0.35.0-alpha.3 h1:aHqVUsi78MIDmMfmMTRMAnpxUlA7poaU1iNXN/sM6gs= +k8s.io/apimachinery v0.35.0-alpha.3/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= +k8s.io/client-go v0.35.0-alpha.3 h1:F7XDcT1E02zv/BeD7Tt1hXJO2aZjIg/jqMZ/oz3yre4= +k8s.io/client-go v0.35.0-alpha.3/go.mod h1:+gl5b5GzUQycBhxcqoQ/dxyFqz4A3Sx9djuc3TckFN8= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/go.mod b/go.mod index aab7831236..d403a378bb 100644 --- a/go.mod +++ b/go.mod @@ -10,30 +10,31 @@ require ( github.com/google/btree v1.1.3 github.com/google/go-cmp v0.7.0 github.com/google/gofuzz v1.2.0 - github.com/onsi/ginkgo/v2 v2.22.0 - github.com/onsi/gomega v1.36.1 + github.com/onsi/ginkgo/v2 v2.27.2 + github.com/onsi/gomega v1.38.2 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 - golang.org/x/mod v0.27.0 - golang.org/x/sync v0.16.0 - golang.org/x/sys v0.35.0 + golang.org/x/mod v0.28.0 + golang.org/x/sync v0.17.0 + golang.org/x/sys v0.37.0 gomodules.xyz/jsonpatch/v2 v2.4.0 gopkg.in/evanphx/json-patch.v4 v4.13.0 // Using v4 to match upstream - k8s.io/api v0.35.0-alpha.1 - k8s.io/apiextensions-apiserver v0.35.0-alpha.1 - k8s.io/apimachinery v0.35.0-alpha.1 - k8s.io/apiserver v0.35.0-alpha.1 - k8s.io/client-go v0.35.0-alpha.1 + k8s.io/api v0.35.0-alpha.3 + k8s.io/apiextensions-apiserver v0.35.0-alpha.3 + k8s.io/apimachinery v0.35.0-alpha.3 + k8s.io/apiserver v0.35.0-alpha.3 + k8s.io/client-go v0.35.0-alpha.3 k8s.io/klog/v2 v2.130.1 - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/structured-merge-diff/v6 v6.3.0 sigs.k8s.io/yaml v1.6.0 ) require ( cel.dev/expr v0.24.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -48,10 +49,9 @@ require ( 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.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/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -79,13 +79,13 @@ require ( go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect @@ -94,7 +94,7 @@ require ( google.golang.org/protobuf v1.36.8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/component-base v0.35.0-alpha.1 // indirect + k8s.io/component-base v0.35.0-alpha.3 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index 836a05ccae..a044979c4d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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= @@ -27,6 +29,12 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -44,8 +52,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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= @@ -59,8 +67,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= @@ -69,10 +77,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -86,6 +94,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -94,10 +106,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -110,8 +122,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0= github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= @@ -131,10 +143,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= @@ -161,55 +179,30 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= @@ -230,24 +223,24 @@ 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.35.0-alpha.1 h1:aL5Q6ZV4MQ2NZMmlnAsV7wj9a30gLhlLnGbx6GUmuBs= -k8s.io/api v0.35.0-alpha.1/go.mod h1:BoZqpN+rs1nX+WI4b+iOCpHIAZT1A5Cx29nfk4Kn4DY= -k8s.io/apiextensions-apiserver v0.35.0-alpha.1 h1:x/nDc4Ic4j9Pjn8trEuRIkbLgVWkSPTNkDWrNGUnCtg= -k8s.io/apiextensions-apiserver v0.35.0-alpha.1/go.mod h1:g00cZRV928nCiZtLlyedrVInFkJJHxzy8QWCyYJslWQ= -k8s.io/apimachinery v0.35.0-alpha.1 h1:FZCO78xXJf7Bb7oLzw5p6nakz/SWaGTi4+IaOl7uAYk= -k8s.io/apimachinery v0.35.0-alpha.1/go.mod h1:1YSL0XujdSTcnuHOR73D16EdW+d49JOdd8TXjCo6Dhc= -k8s.io/apiserver v0.35.0-alpha.1 h1:y30xMnHnusLzP3IU5rn9prng1dBNdWIXWnDbpEKT914= -k8s.io/apiserver v0.35.0-alpha.1/go.mod h1:Xeoi42Em6YeTr+yx3kFByqlCMIP4nbArQBWSblaH7Vs= -k8s.io/client-go v0.35.0-alpha.1 h1:DbQuaoETvFkhWfIckZj3hj1iNnBvEIdiWjSlosmtlX4= -k8s.io/client-go v0.35.0-alpha.1/go.mod h1:CI5Ggq6AukXNEBV2UeBgY4tfrOZfDSa7KuoWwLfHqGA= -k8s.io/component-base v0.35.0-alpha.1 h1:k7wtwWeS+YbH85qfNimsaDOLhnO28wXazq1YTOjnbQI= -k8s.io/component-base v0.35.0-alpha.1/go.mod h1:TczxAPFOtycFi0/MQwZEJAiaGgXb3/XwZib3CgpgA60= +k8s.io/api v0.35.0-alpha.3 h1:BdcXkJ4n/NKhfg06PaSDG8r8Mpe9g3KO9Fkj7B/F8/4= +k8s.io/api v0.35.0-alpha.3/go.mod h1:SArWbUwVv7VhTGGbKX0RoMPXiT6ztjjzkKpRRdl6+E0= +k8s.io/apiextensions-apiserver v0.35.0-alpha.3 h1:Js9dTA0LVvR93tWOZtyJuZKMs0CQuaxNiaM1dwtUM0Q= +k8s.io/apiextensions-apiserver v0.35.0-alpha.3/go.mod h1:LWHywtk0D0qSiJd4Ql65tMY8hDKJYsgBg2jQijeZJNE= +k8s.io/apimachinery v0.35.0-alpha.3 h1:aHqVUsi78MIDmMfmMTRMAnpxUlA7poaU1iNXN/sM6gs= +k8s.io/apimachinery v0.35.0-alpha.3/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= +k8s.io/apiserver v0.35.0-alpha.3 h1:OsYEVXKQgIErQdEO1CSnytsIypIrwdL0kXHaIIPMYso= +k8s.io/apiserver v0.35.0-alpha.3/go.mod h1:w3JHMk6fuQ6QUkWfUoXnurNlbJHleRC8Bk9yE3gWHn8= +k8s.io/client-go v0.35.0-alpha.3 h1:F7XDcT1E02zv/BeD7Tt1hXJO2aZjIg/jqMZ/oz3yre4= +k8s.io/client-go v0.35.0-alpha.3/go.mod h1:+gl5b5GzUQycBhxcqoQ/dxyFqz4A3Sx9djuc3TckFN8= +k8s.io/component-base v0.35.0-alpha.3 h1:1ldfRd8A5qYvjZipXvXdXhBxI8fFN7MYRAHTIrc26T8= +k8s.io/component-base v0.35.0-alpha.3/go.mod h1:DDQa1Mchpy4/+kz0TVlMxSBZyddpnzTHka+N3BnXL3E= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= diff --git a/tools/setup-envtest/go.mod b/tools/setup-envtest/go.mod index 917187b3b0..9b88b1487c 100644 --- a/tools/setup-envtest/go.mod +++ b/tools/setup-envtest/go.mod @@ -5,25 +5,28 @@ go 1.25.0 require ( github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 - github.com/onsi/ginkgo/v2 v2.22.2 - github.com/onsi/gomega v1.36.2 + github.com/onsi/ginkgo/v2 v2.27.2 + github.com/onsi/gomega v1.38.2 github.com/spf13/afero v1.12.0 github.com/spf13/pflag v1.0.9 go.uber.org/zap v1.27.0 - k8s.io/apimachinery v0.35.0-alpha.1 + k8s.io/apimachinery v0.35.0-alpha.3 sigs.k8s.io/yaml v1.6.0 ) require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect go.uber.org/multierr v1.10.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.43.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/tools v0.35.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/protobuf v1.36.8 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/setup-envtest/go.sum b/tools/setup-envtest/go.sum index f5bb7038b3..7bcb4a2fee 100644 --- a/tools/setup-envtest/go.sum +++ b/tools/setup-envtest/go.sum @@ -1,52 +1,87 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= -github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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.35.0-alpha.1 h1:FZCO78xXJf7Bb7oLzw5p6nakz/SWaGTi4+IaOl7uAYk= -k8s.io/apimachinery v0.35.0-alpha.1/go.mod h1:1YSL0XujdSTcnuHOR73D16EdW+d49JOdd8TXjCo6Dhc= +k8s.io/apimachinery v0.35.0-alpha.3 h1:aHqVUsi78MIDmMfmMTRMAnpxUlA7poaU1iNXN/sM6gs= +k8s.io/apimachinery v0.35.0-alpha.3/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 0a0c325b445acb4a4ed1c4868a9daa5e8ca40de3 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sun, 2 Nov 2025 22:30:05 -0500 Subject: [PATCH 46/68] :seedling: Add some more tests to the priorityqueue This change adds a few additional tests to the priorityqueue as well as extends an existing one to test a few more things. All of these already pass. --- .../priorityqueue/priorityqueue_test.go | 182 +++++++++++++----- 1 file changed, 133 insertions(+), 49 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 9c708e982b..f9544e4cb9 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -3,6 +3,7 @@ package priorityqueue import ( "fmt" "math/rand/v2" + "strconv" "sync" "testing" "testing/synctest" @@ -103,6 +104,30 @@ var _ = Describe("Controllerworkqueue", func() { Expect(metrics.adds["test"]).To(Equal(1)) }) + It("enqueues a locked item", func() { + q, metrics := newQueue() + defer q.ShutDown() + + q.AddWithOpts(AddOpts{}, "foo") + Expect(q.Len()).To(Equal(1)) + item, priority, shutdown := q.GetWithPriority() + Expect(item).To(Equal("foo")) + Expect(priority).To(Equal(0)) + Expect(shutdown).To(BeFalse()) + + q.AddWithOpts(AddOpts{}, "foo") + Expect(q.Len()).To(Equal(1)) + q.Done("foo") + + item, priority, shutdown = q.GetWithPriority() + Expect(item).To(Equal("foo")) + Expect(priority).To(Equal(0)) + Expect(shutdown).To(BeFalse()) + + Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) + Expect(metrics.adds["test"]).To(Equal(2)) + }) + It("retains the highest priority", func() { q, metrics := newQueue() defer q.ShutDown() @@ -120,6 +145,23 @@ var _ = Describe("Controllerworkqueue", func() { Expect(metrics.adds["test"]).To(Equal(1)) }) + It("will not decrease the priority", func() { + q, metrics := newQueue() + defer q.ShutDown() + + q.AddWithOpts(AddOpts{Priority: ptr.To(2)}, "foo") + q.AddWithOpts(AddOpts{Priority: ptr.To(1)}, "foo") + + item, priority, _ := q.GetWithPriority() + Expect(item).To(Equal("foo")) + Expect(priority).To(Equal(2)) + + Expect(q.Len()).To(Equal(0)) + + Expect(metrics.depth["test"]).To(Equal(map[int]int{2: 0})) + Expect(metrics.adds["test"]).To(Equal(1)) + }) + It("gets pushed to the front if the priority increases", func() { q, metrics := newQueue() defer q.ShutDown() @@ -251,6 +293,35 @@ var _ = Describe("Controllerworkqueue", func() { Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) metrics.mu.Unlock() }) + + It("updates metrics correctly when an item with after is re-added with higher priority and no after", func() { + q, metrics := newQueue() + defer q.ShutDown() + + q.AddWithOpts(AddOpts{After: time.Hour, Priority: ptr.To(0)}, "foo") + Expect(q.Len()).To(Equal(0)) + metrics.mu.Lock() + Expect(metrics.depth["test"]).To(Equal(map[int]int{})) + metrics.mu.Unlock() + + q.AddWithOpts(AddOpts{Priority: ptr.To(1)}, "foo") + + Expect(q.Len()).To(Equal(1)) + metrics.mu.Lock() + Expect(metrics.depth["test"]).To(Equal(map[int]int{1: 1})) + metrics.mu.Unlock() + + item, priority, _ := q.GetWithPriority() + Expect(item).To(Equal("foo")) + Expect(priority).To(Equal(1)) + Expect(q.Len()).To(Equal(0)) + + metrics.mu.Lock() + Expect(metrics.depth["test"][1]).To(Equal(0)) + for _, depth := range metrics.depth["test"] { + Expect(depth).To(Equal(0)) + } + }) }) func BenchmarkAddGetDone(b *testing.B) { @@ -443,56 +514,69 @@ func newQueueWithTimeForwarder() (_ *priorityqueue[string], _ *fakeMetricsProvid func TestHighPriorityItemsAreReturnedBeforeLowPriorityItemMultipleTimes(t *testing.T) { t.Parallel() - synctest.Test(t, func(t *testing.T) { - g := NewWithT(t) - - q, metrics := newQueue() - defer q.ShutDown() - - const itemsPerPriority = 1000 - lowPriority := 0 - lowMiddlePriority := 5 - middlePriority := 10 - upperMiddlePriority := 15 - highPriority := 20 - for i := range itemsPerPriority { - q.AddWithOpts(AddOpts{Priority: &highPriority}, fmt.Sprintf("high-%d", i)) - q.AddWithOpts(AddOpts{Priority: &upperMiddlePriority}, fmt.Sprintf("upperMiddle-%d", i)) - q.AddWithOpts(AddOpts{Priority: &middlePriority}, fmt.Sprintf("middle-%d", i)) - q.AddWithOpts(AddOpts{Priority: &lowMiddlePriority}, fmt.Sprintf("lowMiddle-%d", i)) - q.AddWithOpts(AddOpts{Priority: &lowPriority}, fmt.Sprintf("low-%d", i)) - } - synctest.Wait() + for _, after := range []time.Duration{-time.Second, 0, time.Second} { + t.Run(fmt.Sprintf("after=%v", after), func(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics, forwardQueueTimeBy := newQueueWithTimeForwarder() + defer q.ShutDown() + + const itemsPerPriority = 1000 + lowPriority := -10 + lowMiddlePriority := -5 + middlePriority := 0 + upperMiddlePriority := 5 + highPriority := 10 + for i := range itemsPerPriority { + q.AddWithOpts(AddOpts{Priority: &highPriority, After: after}, fmt.Sprintf("high-%d", i)) + q.AddWithOpts(AddOpts{Priority: &upperMiddlePriority, After: after}, fmt.Sprintf("upperMiddle-%d", i)) + q.AddWithOpts(AddOpts{Priority: &middlePriority, After: after}, fmt.Sprintf("middle-%d", i)) + q.AddWithOpts(AddOpts{Priority: &lowMiddlePriority, After: after}, fmt.Sprintf("lowMiddle-%d", i)) + q.AddWithOpts(AddOpts{Priority: &lowPriority, After: after}, fmt.Sprintf("low-%d", i)) + } + synctest.Wait() + if after > 0 { + forwardQueueTimeBy(after) + synctest.Wait() + } - for range itemsPerPriority { - key, prio, _ := q.GetWithPriority() - g.Expect(prio).To(Equal(highPriority)) - g.Expect(key).To(HavePrefix("high-")) - } - for range itemsPerPriority { - key, prio, _ := q.GetWithPriority() - g.Expect(prio).To(Equal(upperMiddlePriority)) - g.Expect(key).To(HavePrefix("upperMiddle-")) - } - for range itemsPerPriority { - key, prio, _ := q.GetWithPriority() - g.Expect(prio).To(Equal(middlePriority)) - g.Expect(key).To(HavePrefix("middle-")) - } - for range itemsPerPriority { - key, prio, _ := q.GetWithPriority() - g.Expect(prio).To(Equal(lowMiddlePriority)) - g.Expect(key).To(HavePrefix("lowMiddle-")) - } - for range itemsPerPriority { - key, prio, _ := q.GetWithPriority() - g.Expect(prio).To(Equal(lowPriority)) - g.Expect(key).To(HavePrefix("low-")) - } - g.Expect(metrics.depth["test"]).To(Equal(map[int]int{10: 0, 5: 0, 0: 0, 20: 0, 15: 0})) - g.Expect(metrics.adds["test"]).To(Equal(itemsPerPriority * 5)) - g.Expect(metrics.retries["test"]).To(Equal(0)) - }) + for i := range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(highPriority)) + g.Expect(key).To(Equal("high-" + strconv.Itoa(i))) + } + for i := range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(upperMiddlePriority)) + g.Expect(key).To(Equal("upperMiddle-" + strconv.Itoa(i))) + } + for i := range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(middlePriority)) + g.Expect(key).To(Equal("middle-" + strconv.Itoa(i))) + } + for i := range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(lowMiddlePriority)) + g.Expect(key).To(Equal("lowMiddle-" + strconv.Itoa(i))) + } + for i := range itemsPerPriority { + key, prio, _ := q.GetWithPriority() + g.Expect(prio).To(Equal(lowPriority)) + g.Expect(key).To(Equal("low-" + strconv.Itoa(i))) + } + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{-10: 0, -5: 0, 0: 0, 5: 0, 10: 0})) + g.Expect(metrics.adds["test"]).To(Equal(itemsPerPriority * 5)) + expectedRetries := 0 + if after > 0 { + expectedRetries = itemsPerPriority * 5 + } + g.Expect(metrics.retries["test"]).To(Equal(expectedRetries)) + }) + }) + } } func newQueue() (*priorityqueue[string], *fakeMetricsProvider) { From c9305f08ee20e1e98186989f70fc22e04cd18520 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 20:03:54 +0000 Subject: [PATCH 47/68] :seedling: Bump actions/checkout in the all-github-actions group Bumps the all-github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 5.0.0 to 5.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...93cb6efe18208431cddfb8368fd83d5badbf9bfd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/ossf-scorecard.yaml | 2 +- .github/workflows/pr-dependabot.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/verify.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 65e849da2d..0938cf226f 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 - 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 379fb88557..29fe19990a 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 with: persist-credentials: false diff --git a/.github/workflows/pr-dependabot.yaml b/.github/workflows/pr-dependabot.yaml index 10162e9129..3196654902 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 - 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 ce3d7ad462..cf4755df09 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 - 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 2168d72516..a0871ec070 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 - name: Check if PR title is valid env: From a0b87dc542a69124d2df2d5eb46f6aaf4c304cb3 Mon Sep 17 00:00:00 2001 From: dongjiang1989 Date: Wed, 12 Nov 2025 20:00:49 +0800 Subject: [PATCH 48/68] update golangci-lint version and add modernize lint Signed-off-by: dongjiang1989 by codereview Signed-off-by: dongjiang1989 --- .github/workflows/golangci-lint.yml | 2 +- .golangci.yml | 5 + examples/crd/pkg/zz_generated.deepcopy.go | 1 + hack/tools/cmd/gomodcheck/main.go | 2 +- pkg/builder/controller.go | 2 +- pkg/builder/controller_test.go | 4 +- pkg/builder/webhook.go | 7 +- pkg/builder/webhook_test.go | 4 + pkg/cache/cache_test.go | 36 +-- pkg/cache/defaulting_test.go | 8 +- pkg/cache/delegating_by_gvk_cache.go | 6 +- pkg/cache/informer_cache.go | 2 +- pkg/cache/internal/cache_reader.go | 16 +- pkg/cache/internal/informers.go | 6 +- pkg/certwatcher/certwatcher_test.go | 2 +- pkg/client/apiutil/apimachinery.go | 2 +- pkg/client/apiutil/restmapper_test.go | 8 +- pkg/client/client_test.go | 4 +- pkg/client/example_test.go | 22 +- pkg/client/fake/client.go | 41 +--- pkg/client/fake/client_test.go | 52 ++-- pkg/client/namespaced_client_test.go | 6 +- pkg/client/patch.go | 6 +- .../priorityqueue/priorityqueue_test.go | 39 ++- pkg/envtest/komega/equalobject.go | 10 +- pkg/envtest/komega/equalobject_test.go | 222 +++++++++--------- pkg/handler/eventhandler_test.go | 10 +- pkg/internal/controller/controller_test.go | 10 +- pkg/internal/flock/flock_other.go | 2 +- pkg/internal/flock/flock_unix.go | 1 - .../recorder/recorder_integration_test.go | 4 +- pkg/internal/source/event_handler.go | 6 +- pkg/internal/testing/addr/manager.go | 2 +- pkg/internal/testing/certs/tinyca_test.go | 2 +- pkg/internal/testing/process/arguments.go | 4 +- .../testing/process/procattr_other.go | 1 - pkg/internal/testing/process/procattr_unix.go | 1 - pkg/leaderelection/leader_election.go | 5 +- pkg/log/deleg.go | 10 +- pkg/log/log.go | 2 +- pkg/log/log_test.go | 30 +-- pkg/log/null.go | 6 +- pkg/log/zap/zap_test.go | 50 ++-- pkg/manager/runnable_group_test.go | 10 +- pkg/manager/signals/signal_posix.go | 1 - pkg/manager/signals/signal_test.go | 2 +- pkg/scheme/scheme_test.go | 12 +- pkg/source/source_integration_test.go | 8 +- pkg/webhook/admission/decode.go | 2 +- pkg/webhook/admission/decode_test.go | 4 +- pkg/webhook/admission/defaulter_custom.go | 1 + pkg/webhook/admission/validator_custom.go | 2 + pkg/webhook/alias.go | 6 +- tools/setup-envtest/env/exit.go | 8 +- 54 files changed, 347 insertions(+), 370 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 65e849da2d..30c7f7f02a 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -34,6 +34,6 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # tag=v9.0.0 with: - version: v2.5.0 + version: v2.6.1 args: --output.text.print-linter-name=true --output.text.colors=true --timeout 10m working-directory: ${{matrix.working-directory}} diff --git a/.golangci.yml b/.golangci.yml index 88fa35359e..9c11b8e816 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -31,6 +31,7 @@ linters: - iotamixing - makezero - misspell + - modernize - nakedret - nilerr - nolintlint @@ -75,6 +76,10 @@ linters: - pkg: sigs.k8s.io/controller-runtime alias: ctrl no-unaliased: true + modernize: + disable: + - omitzero + - fmtappendf revive: rules: # The following rules are recommended https://github.com/mgechev/revive#recommended-configuration diff --git a/examples/crd/pkg/zz_generated.deepcopy.go b/examples/crd/pkg/zz_generated.deepcopy.go index cd506a87c0..d2ba59e26d 100644 --- a/examples/crd/pkg/zz_generated.deepcopy.go +++ b/examples/crd/pkg/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/hack/tools/cmd/gomodcheck/main.go b/hack/tools/cmd/gomodcheck/main.go index 5cbaf377e2..afba18b624 100644 --- a/hack/tools/cmd/gomodcheck/main.go +++ b/hack/tools/cmd/gomodcheck/main.go @@ -162,7 +162,7 @@ func modulesFromUpstreamModGraph(upstreamRefList []string) (map[string]map[strin } modToVersionToUpstreamRef := make(map[string]map[string]string) - for _, line := range strings.Split(graph, "\n") { + for line := range strings.SplitSeq(graph, "\n") { ref := strings.SplitN(line, "@", 2)[0] if _, ok := upstreamRefs[ref]; !ok { diff --git a/pkg/builder/controller.go b/pkg/builder/controller.go index 6d906f6e52..840e27b679 100644 --- a/pkg/builder/controller.go +++ b/pkg/builder/controller.go @@ -312,7 +312,7 @@ func (blder *TypedBuilder[request]) doWatch() error { return err } - if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) { + if reflect.TypeFor[request]() != reflect.TypeFor[reconcile.Request]() { return fmt.Errorf("For() can only be used with reconcile.Request, got %T", *new(request)) } diff --git a/pkg/builder/controller_test.go b/pkg/builder/controller_test.go index 46e937d590..1f9729d9f4 100644 --- a/pkg/builder/controller_test.go +++ b/pkg/builder/controller_test.go @@ -61,10 +61,10 @@ func (l *testLogger) Enabled(int) bool { return true } -func (l *testLogger) Info(level int, msg string, keysAndValues ...interface{}) { +func (l *testLogger) Info(level int, msg string, keysAndValues ...any) { } -func (l *testLogger) WithValues(keysAndValues ...interface{}) logr.LogSink { +func (l *testLogger) WithValues(keysAndValues ...any) logr.LogSink { return l } diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go index 428100a66c..d9c57c5e8b 100644 --- a/pkg/builder/webhook.go +++ b/pkg/builder/webhook.go @@ -39,10 +39,10 @@ import ( // WebhookBuilder builds a Webhook. type WebhookBuilder[T runtime.Object] struct { apiType runtime.Object - customDefaulter admission.CustomDefaulter + customDefaulter admission.CustomDefaulter //nolint:staticcheck defaulter admission.Defaulter[T] customDefaulterOpts []admission.DefaulterOption - customValidator admission.CustomValidator + customValidator admission.CustomValidator //nolint:staticcheck validator admission.Validator[T] customPath string customValidatorCustomPath string @@ -64,6 +64,7 @@ func WebhookManagedBy[T runtime.Object](m manager.Manager, object T) *WebhookBui // WithCustomDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption) // will be wired for this type. +// // Deprecated: Use WithDefaulter instead. func (blder *WebhookBuilder[T]) WithCustomDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder[T] { blder.customDefaulter = defaulter @@ -79,6 +80,7 @@ func (blder *WebhookBuilder[T]) WithDefaulter(defaulter admission.Defaulter[T], } // WithCustomValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type. +// // Deprecated: Use WithValidator instead. func (blder *WebhookBuilder[T]) WithCustomValidator(validator admission.CustomValidator) *WebhookBuilder[T] { blder.customValidator = validator @@ -306,6 +308,7 @@ func (blder *WebhookBuilder[T]) getValidatingWebhook() (*admission.Webhook, erro } w = admission.WithValidator(blder.mgr.GetScheme(), blder.validator) } else if blder.customValidator != nil { + //nolint:staticcheck w = admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, blder.customValidator) } if w != nil && blder.recoverPanic != nil { diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go index e10e693ab8..55c3e11817 100644 --- a/pkg/builder/webhook_test.go +++ b/pkg/builder/webhook_test.go @@ -1180,6 +1180,7 @@ func (*testDefaulter) Default(ctx context.Context, obj *TestDefaulterObject) err return nil } +//nolint:staticcheck var _ admission.CustomDefaulter = &TestCustomDefaulter{} type TestCustomValidator struct{} @@ -1263,6 +1264,7 @@ func (*testValidator) ValidateDelete(ctx context.Context, obj *TestValidatorObje return nil, nil } +//nolint:staticcheck var _ admission.CustomValidator = &TestCustomValidator{} // TestCustomDefaultValidator for default @@ -1286,6 +1288,7 @@ func (*TestCustomDefaultValidator) Default(ctx context.Context, obj runtime.Obje return nil } +//nolint:staticcheck var _ admission.CustomDefaulter = &TestCustomDefaulter{} // TestCustomDefaultValidator for validation @@ -1345,6 +1348,7 @@ func (*TestCustomDefaultValidator) ValidateDelete(ctx context.Context, obj runti return nil, nil } +//nolint:staticcheck var _ admission.CustomValidator = &TestCustomValidator{} type testValidatorDefaulter struct{} diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index c2dae0978f..a95836b90d 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -216,7 +216,7 @@ var _ = Describe("Cache with transformers", func() { By("creating the informer cache") informerCache, err = cache.New(cfg, cache.Options{ - DefaultTransform: func(i interface{}) (interface{}, error) { + DefaultTransform: func(i any) (any, error) { obj := i.(runtime.Object) Expect(obj).NotTo(BeNil()) @@ -238,7 +238,7 @@ var _ = Describe("Cache with transformers", func() { }, ByObject: map[client.Object]cache.ByObject{ &corev1.Pod{}: { - Transform: func(i interface{}) (interface{}, error) { + Transform: func(i any) (any, error) { obj := i.(runtime.Object) Expect(obj).NotTo(BeNil()) accessor, err := meta.Accessor(obj) @@ -1103,7 +1103,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(out).To(Equal(uKnownPod2)) By("altering a field in the retrieved pod") - m, _ := out.Object["spec"].(map[string]interface{}) + m, _ := out.Object["spec"].(map[string]any) m["activeDeadlineSeconds"] = 4 By("verifying the pods are no longer equal") @@ -1954,8 +1954,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(sii.HasSynced()).To(BeTrue()) By("adding an event handler listening for object creation which sends the object to a channel") - out := make(chan interface{}) - addFunc := func(obj interface{}) { + out := make(chan any) + addFunc := func(obj any) { out <- obj } _, _ = sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) @@ -2014,8 +2014,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(sii.HasSynced()).To(BeTrue()) By("adding an event handler listening for object creation which sends the object to a channel") - out := make(chan interface{}) - addFunc := func(obj interface{}) { + out := make(chan any) + addFunc := func(obj any) { out <- obj } _, _ = sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) @@ -2196,9 +2196,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca By("getting a shared index informer for a pod") pod := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "containers": []map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "containers": []map[string]any{ { "name": "nginx", "image": "nginx", @@ -2220,8 +2220,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(sii.HasSynced()).To(BeTrue()) By("adding an event handler listening for object creation which sends the object to a channel") - out := make(chan interface{}) - addFunc := func(obj interface{}) { + out := make(chan any) + addFunc := func(obj any) { out <- obj } _, _ = sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) @@ -2239,9 +2239,9 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca It("should be able to stop and restart informers", func(ctx SpecContext) { By("getting a shared index informer for a pod") pod := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "containers": []map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "containers": []map[string]any{ { "name": "nginx", "image": "nginx", @@ -2294,7 +2294,7 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca if !ok { return []string{} } - m, ok := s.(map[string]interface{}) + m, ok := s.(map[string]any) if !ok { return []string{} } @@ -2379,8 +2379,8 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca Expect(sii.HasSynced()).To(BeTrue()) By("adding an event handler listening for object creation which sends the object to a channel") - out := make(chan interface{}) - addFunc := func(obj interface{}) { + out := make(chan any) + addFunc := func(obj any) { out <- obj } _, _ = sii.AddEventHandler(kcache.ResourceEventHandlerFuncs{AddFunc: addFunc}) diff --git a/pkg/cache/defaulting_test.go b/pkg/cache/defaulting_test.go index 89a0334324..b4b4dd030a 100644 --- a/pkg/cache/defaulting_test.go +++ b/pkg/cache/defaulting_test.go @@ -474,11 +474,9 @@ func TestDefaultOptsRace(t *testing.T) { // Start go routines which re-use the above options struct. wg := sync.WaitGroup{} for range 2 { - wg.Add(1) - go func() { + wg.Go(func() { _, _ = defaultOpts(&rest.Config{}, opts) - wg.Done() - }() + }) } // Wait for the go routines to finish. @@ -509,7 +507,7 @@ func TestDefaultConfigConsidersAllFields(t *testing.T) { }, ) - for i := 0; i < 100; i++ { + for range 100 { fuzzed := Config{} f.Fuzz(&fuzzed) diff --git a/pkg/cache/delegating_by_gvk_cache.go b/pkg/cache/delegating_by_gvk_cache.go index 46bd243c66..adc5d957a4 100644 --- a/pkg/cache/delegating_by_gvk_cache.go +++ b/pkg/cache/delegating_by_gvk_cache.go @@ -81,13 +81,11 @@ func (dbt *delegatingByGVKCache) Start(ctx context.Context) error { errs := make(chan error) for idx := range allCaches { cache := allCaches[idx] - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := cache.Start(ctx); err != nil { errs <- err } - }() + }) } select { diff --git a/pkg/cache/informer_cache.go b/pkg/cache/informer_cache.go index 091667b7fa..5f0d88fdb0 100644 --- a/pkg/cache/informer_cache.go +++ b/pkg/cache/informer_cache.go @@ -221,7 +221,7 @@ func (ic *informerCache) IndexField(ctx context.Context, obj client.Object, fiel } func indexByField(informer Informer, field string, extractValue client.IndexerFunc) error { - indexFunc := func(objRaw interface{}) ([]string, error) { + indexFunc := func(objRaw any) ([]string, error) { // TODO(directxman12): check if this is the correct type? obj, isObj := objRaw.(client.Object) if !isObj { diff --git a/pkg/cache/internal/cache_reader.go b/pkg/cache/internal/cache_reader.go index eb6b544855..624869f590 100644 --- a/pkg/cache/internal/cache_reader.go +++ b/pkg/cache/internal/cache_reader.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "reflect" + "slices" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -109,7 +110,7 @@ func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Ob // List lists items out of the indexer and writes them to out. func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...client.ListOption) error { - var objs []interface{} + var objs []any var err error listOpts := client.ListOptions{} @@ -186,10 +187,10 @@ func (c *CacheReader) List(_ context.Context, out client.ObjectList, opts ...cli return nil } -func byIndexes(indexer cache.Indexer, requires fields.Requirements, namespace string) ([]interface{}, error) { +func byIndexes(indexer cache.Indexer, requires fields.Requirements, namespace string) ([]any, error) { var ( err error - objs []interface{} + objs []any vals []string ) indexers := indexer.GetIndexers() @@ -213,17 +214,14 @@ func byIndexes(indexer cache.Indexer, requires fields.Requirements, namespace st if !exist { return nil, fmt.Errorf("index with name %s does not exist", indexName) } - filteredObjects := make([]interface{}, 0, len(objs)) + filteredObjects := make([]any, 0, len(objs)) for _, obj := range objs { vals, err = fn(obj) if err != nil { return nil, err } - for _, val := range vals { - if val == indexedValue { - filteredObjects = append(filteredObjects, obj) - break - } + if slices.Contains(vals, indexedValue) { + filteredObjects = append(filteredObjects, obj) } } if len(filteredObjects) == 0 { diff --git a/pkg/cache/internal/informers.go b/pkg/cache/internal/informers.go index f216be0d9e..0f921ef63d 100644 --- a/pkg/cache/internal/informers.go +++ b/pkg/cache/internal/informers.go @@ -242,11 +242,9 @@ func (ip *Informers) startInformerLocked(cacheEntry *Cache) { return } - ip.waitGroup.Add(1) - go func() { - defer ip.waitGroup.Done() + ip.waitGroup.Go(func() { cacheEntry.Start(ip.ctx.Done()) - }() + }) } func (ip *Informers) waitForStarted(ctx context.Context) bool { diff --git a/pkg/certwatcher/certwatcher_test.go b/pkg/certwatcher/certwatcher_test.go index 9737925a6b..ba47afc112 100644 --- a/pkg/certwatcher/certwatcher_test.go +++ b/pkg/certwatcher/certwatcher_test.go @@ -255,7 +255,7 @@ var _ = Describe("CertWatcher", func() { }) func writeCerts(certPath, keyPath, ip string) error { - var priv interface{} + var priv any var err error priv, err = rsa.GenerateKey(rand.Reader, 2048) if err != nil { diff --git a/pkg/client/apiutil/apimachinery.go b/pkg/client/apiutil/apimachinery.go index b132cb2d4d..217990dece 100644 --- a/pkg/client/apiutil/apimachinery.go +++ b/pkg/client/apiutil/apimachinery.go @@ -231,7 +231,7 @@ func (t targetZeroingDecoder) Decode(data []byte, defaults *schema.GroupVersionK } // zero zeros the value of a pointer. -func zero(x interface{}) { +func zero(x any) { if x == nil { return } diff --git a/pkg/client/apiutil/restmapper_test.go b/pkg/client/apiutil/restmapper_test.go index 51807f12de..71fc1681db 100644 --- a/pkg/client/apiutil/restmapper_test.go +++ b/pkg/client/apiutil/restmapper_test.go @@ -746,7 +746,7 @@ func TestLazyRestMapperProvider(t *testing.T) { wg := sync.WaitGroup{} wg.Add(50) - for i := 0; i < 50; i++ { + for range 50 { go func() { defer wg.Done() httpClient, err := rest.HTTPClientFor(restCfg) @@ -811,7 +811,7 @@ type errorMatcher struct { message string } -func (e *errorMatcher) Match(actual interface{}) (success bool, err error) { +func (e *errorMatcher) Match(actual any) (success bool, err error) { if actual == nil { return false, nil } @@ -824,10 +824,10 @@ func (e *errorMatcher) Match(actual interface{}) (success bool, err error) { return e.checkFunc(actualErr), nil } -func (e *errorMatcher) FailureMessage(actual interface{}) (message string) { +func (e *errorMatcher) FailureMessage(actual any) (message string) { return format.Message(actual, fmt.Sprintf("to be %s error", e.message)) } -func (e *errorMatcher) NegatedFailureMessage(actual interface{}) (message string) { +func (e *errorMatcher) NegatedFailureMessage(actual any) (message string) { return format.Message(actual, fmt.Sprintf("not to be %s error", e.message)) } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 079458f527..593a2b3c18 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -97,7 +97,7 @@ func deleteNamespace(ctx context.Context, ns *corev1.Namespace) { Expect(err).NotTo(HaveOccurred()) WAIT_LOOP: - for i := 0; i < 10; i++ { + for range 10 { ns, err = clientset.CoreV1().Namespaces().Get(ctx, ns.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { // success! @@ -1215,7 +1215,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC dep, err := clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) dep.APIVersion = appsv1.SchemeGroupVersion.String() - dep.Kind = reflect.TypeOf(dep).Elem().Name() + dep.Kind = reflect.TypeFor[appsv1.Deployment]().Name() depUnstructured, err := toUnstructured(dep) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/client/example_test.go b/pkg/client/example_test.go index 390dc10143..6b4616a8a1 100644 --- a/pkg/client/example_test.go +++ b/pkg/client/example_test.go @@ -122,24 +122,24 @@ func ExampleClient_create() { // Using a unstructured object. u := &unstructured.Unstructured{} - u.Object = map[string]interface{}{ - "metadata": map[string]interface{}{ + u.Object = map[string]any{ + "metadata": map[string]any{ "name": "name", "namespace": "namespace", }, - "spec": map[string]interface{}{ + "spec": map[string]any{ "replicas": 2, - "selector": map[string]interface{}{ - "matchLabels": map[string]interface{}{ + "selector": map[string]any{ + "matchLabels": map[string]any{ "foo": "bar", }, }, - "template": map[string]interface{}{ - "labels": map[string]interface{}{ + "template": map[string]any{ + "labels": map[string]any{ "foo": "bar", }, - "spec": map[string]interface{}{ - "containers": []map[string]interface{}{ + "spec": map[string]any{ + "containers": []map[string]any{ { "name": "nginx", "image": "nginx", @@ -227,8 +227,8 @@ func ExampleClient_apply() { // This example shows how to use the client with typed and unstructured objects to patch objects' status. func ExampleClient_patchStatus() { u := &unstructured.Unstructured{} - u.Object = map[string]interface{}{ - "metadata": map[string]interface{}{ + u.Object = map[string]any{ + "metadata": map[string]any{ "name": "foo", "namespace": "namespace", }, diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 05d71bac76..2a07bd40b2 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "reflect" + "slices" "strings" "sync" "time" @@ -588,13 +589,7 @@ func (c *fakeClient) objMatchesFieldSelector(o runtime.Object, extractIndex clie panic(fmt.Errorf("expected object %v to be of type client.Object, but it's not", o)) } - for _, extractedVal := range extractIndex(obj) { - if extractedVal == val { - return true - } - } - - return false + return slices.Contains(extractIndex(obj), val) } func (c *fakeClient) Scheme() *runtime.Scheme { @@ -626,10 +621,8 @@ func (c *fakeClient) Create(ctx context.Context, obj client.Object, opts ...clie createOptions := &client.CreateOptions{} createOptions.ApplyOptions(opts) - for _, dryRunOpt := range createOptions.DryRun { - if dryRunOpt == metav1.DryRunAll { - return nil - } + if slices.Contains(createOptions.DryRun, metav1.DryRunAll) { + return nil } gvr, err := getGVRFromObject(obj, c.scheme) @@ -693,10 +686,8 @@ func (c *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...clie delOptions := client.DeleteOptions{} delOptions.ApplyOptions(opts) - for _, dryRunOpt := range delOptions.DryRun { - if dryRunOpt == metav1.DryRunAll { - return nil - } + if slices.Contains(delOptions.DryRun, metav1.DryRunAll) { + return nil } c.trackerWriteLock.Lock() @@ -742,10 +733,8 @@ func (c *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts .. dcOptions := client.DeleteAllOfOptions{} dcOptions.ApplyOptions(opts) - for _, dryRunOpt := range dcOptions.DryRun { - if dryRunOpt == metav1.DryRunAll { - return nil - } + if slices.Contains(dcOptions.DryRun, metav1.DryRunAll) { + return nil } c.trackerWriteLock.Lock() @@ -793,10 +782,8 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd updateOptions := &client.UpdateOptions{} updateOptions.ApplyOptions(opts) - for _, dryRunOpt := range updateOptions.DryRun { - if dryRunOpt == metav1.DryRunAll { - return nil - } + if slices.Contains(updateOptions.DryRun, metav1.DryRunAll) { + return nil } gvr, err := getGVRFromObject(obj, c.scheme) @@ -908,10 +895,8 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client c.schemeLock.RLock() defer c.schemeLock.RUnlock() - for _, dryRunOpt := range patchOptions.DryRun { - if dryRunOpt == metav1.DryRunAll { - return nil - } + if slices.Contains(patchOptions.DryRun, metav1.DryRunAll) { + return nil } gvr, err := getGVRFromObject(obj, c.scheme) @@ -1507,7 +1492,7 @@ func inTreeResourcesWithStatus() []schema.GroupVersionKind { } // zero zeros the value of a pointer. -func zero(x interface{}) { +func zero(x any) { if x == nil { return } diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 1eed4d5a6d..1901b3051d 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -857,7 +857,7 @@ var _ = Describe("Fake client", func() { }) It("should handle finalizers deleting a collection", func(ctx SpecContext) { - for i := 0; i < 5; i++ { + for i := range 5 { namespacedName := types.NamespacedName{ Name: fmt.Sprintf("test-cm-%d", i), Namespace: "delete-collection-with-finalizers", @@ -992,9 +992,9 @@ var _ = Describe("Fake client", func() { It("should be able to Patch", func(ctx SpecContext) { By("Patching a deployment") - mergePatch, err := json.Marshal(map[string]interface{}{ - "metadata": map[string]interface{}{ - "annotations": map[string]interface{}{ + mergePatch, err := json.Marshal(map[string]any{ + "metadata": map[string]any{ + "annotations": map[string]any{ "foo": "bar", }, }, @@ -1209,8 +1209,8 @@ var _ = Describe("Fake client", func() { Expect(err).ToNot(HaveOccurred()) By("Removing the finalizer") - mergePatch, err := json.Marshal(map[string]interface{}{ - "metadata": map[string]interface{}{ + mergePatch, err := json.Marshal(map[string]any{ + "metadata": map[string]any{ "$deleteFromPrimitiveList/finalizers": []string{ "finalizers.sigs.k8s.io/test", }, @@ -2012,7 +2012,7 @@ var _ = Describe("Fake client", func() { Expect(cl.Create(ctx, obj)).To(Succeed()) original := obj.DeepCopy() - err := unstructured.SetNestedField(obj.Object, map[string]interface{}{"count": int64(2)}, "status") + err := unstructured.SetNestedField(obj.Object, map[string]any{"count": int64(2)}, "status") Expect(err).ToNot(HaveOccurred()) Expect(cl.Patch(ctx, obj, client.MergeFrom(original))).To(Succeed()) @@ -3092,12 +3092,12 @@ var _ = Describe("Fake client", func() { }) It("will error out if an object with invalid managedFields is added", func(ctx SpecContext) { - fieldV1Map := map[string]interface{}{ - "f:metadata": map[string]interface{}{ - "f:name": map[string]interface{}{}, - "f:labels": map[string]interface{}{}, - "f:annotations": map[string]interface{}{}, - "f:finalizers": map[string]interface{}{}, + fieldV1Map := map[string]any{ + "f:metadata": map[string]any{ + "f:name": map[string]any{}, + "f:labels": map[string]any{}, + "f:annotations": map[string]any{}, + "f:finalizers": map[string]any{}, }, } fieldV1, err := json.Marshal(fieldV1Map) @@ -3120,12 +3120,12 @@ var _ = Describe("Fake client", func() { }) It("allows adding an object with managedFields", func(ctx SpecContext) { - fieldV1Map := map[string]interface{}{ - "f:metadata": map[string]interface{}{ - "f:name": map[string]interface{}{}, - "f:labels": map[string]interface{}{}, - "f:annotations": map[string]interface{}{}, - "f:finalizers": map[string]interface{}{}, + fieldV1Map := map[string]any{ + "f:metadata": map[string]any{ + "f:name": map[string]any{}, + "f:labels": map[string]any{}, + "f:annotations": map[string]any{}, + "f:finalizers": map[string]any{}, }, } fieldV1, err := json.Marshal(fieldV1Map) @@ -3147,12 +3147,12 @@ var _ = Describe("Fake client", func() { }) It("allows adding an object with invalid managedFields when not using the FieldManagedObjectTracker", func(ctx SpecContext) { - fieldV1Map := map[string]interface{}{ - "f:metadata": map[string]interface{}{ - "f:name": map[string]interface{}{}, - "f:labels": map[string]interface{}{}, - "f:annotations": map[string]interface{}{}, - "f:finalizers": map[string]interface{}{}, + fieldV1Map := map[string]any{ + "f:metadata": map[string]any{ + "f:name": map[string]any{}, + "f:labels": map[string]any{}, + "f:annotations": map[string]any{}, + "f:finalizers": map[string]any{}, }, } fieldV1, err := json.Marshal(fieldV1Map) @@ -3270,7 +3270,7 @@ var _ = Describe("Fake client", func() { } }) -type Schemaless map[string]interface{} +type Schemaless map[string]any type WithSchemalessSpec struct { metav1.TypeMeta `json:",inline"` diff --git a/pkg/client/namespaced_client_test.go b/pkg/client/namespaced_client_test.go index 6e9635474e..d7be6eeee6 100644 --- a/pkg/client/namespaced_client_test.go +++ b/pkg/client/namespaced_client_test.go @@ -706,9 +706,9 @@ var _ = Describe("NamespacedClient", func() { }) func generatePatch() []byte { - mergePatch, err := json.Marshal(map[string]interface{}{ - "metadata": map[string]interface{}{ - "annotations": map[string]interface{}{ + mergePatch, err := json.Marshal(map[string]any{ + "metadata": map[string]any{ + "annotations": map[string]any{ "foo": "bar", }, }, diff --git a/pkg/client/patch.go b/pkg/client/patch.go index 9bd0953fdc..3d914eea22 100644 --- a/pkg/client/patch.go +++ b/pkg/client/patch.go @@ -88,7 +88,7 @@ type MergeFromOptions struct { type mergeFromPatch struct { patchType types.PatchType - createPatch func(originalJSON, modifiedJSON []byte, dataStruct interface{}) ([]byte, error) + createPatch func(originalJSON, modifiedJSON []byte, dataStruct any) ([]byte, error) from Object opts MergeFromOptions } @@ -134,11 +134,11 @@ func (s *mergeFromPatch) Data(obj Object) ([]byte, error) { return data, nil } -func createMergePatch(originalJSON, modifiedJSON []byte, _ interface{}) ([]byte, error) { +func createMergePatch(originalJSON, modifiedJSON []byte, _ any) ([]byte, error) { return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON) } -func createStrategicMergePatch(originalJSON, modifiedJSON []byte, dataStruct interface{}) ([]byte, error) { +func createStrategicMergePatch(originalJSON, modifiedJSON []byte, dataStruct any) ([]byte, error) { return strategicpatch.CreateTwoWayMergePatch(originalJSON, modifiedJSON, dataStruct) } diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 9c708e982b..40aeccb2e3 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -209,14 +209,12 @@ var _ = Describe("Controllerworkqueue", func() { wg := sync.WaitGroup{} for range 100 { // The panic only occurred relatively frequently with a high number of go routines. - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for range 10 { obj, _, _ := q.GetWithPriority() q.Done(obj) } - }() + }) } wg.Wait() @@ -256,9 +254,9 @@ var _ = Describe("Controllerworkqueue", func() { func BenchmarkAddGetDone(b *testing.B) { q := New[int]("") defer q.ShutDown() - b.ResetTimer() - for n := 0; n < b.N; n++ { - for i := 0; i < 1000; i++ { + + for b.Loop() { + for i := range 1000 { q.Add(i) } for range 1000 { @@ -271,9 +269,9 @@ func BenchmarkAddGetDone(b *testing.B) { func BenchmarkAddOnly(b *testing.B) { q := New[int]("") defer q.ShutDown() - b.ResetTimer() - for n := 0; n < b.N; n++ { - for i := 0; i < 1000; i++ { + + for b.Loop() { + for i := range 1000 { q.Add(i) } } @@ -288,9 +286,9 @@ func BenchmarkAddLockContended(b *testing.B) { q.Done(item) } }() - b.ResetTimer() - for n := 0; n < b.N; n++ { - for i := 0; i < 1000; i++ { + + for b.Loop() { + for i := range 1000 { q.Add(i) } } @@ -329,10 +327,7 @@ func TestFuzzPriorityQueue(t *testing.T) { q, metrics := newQueue() for range 10 { - wg.Add(1) - go func() { - defer wg.Done() - + wg.Go(func() { for range 1000 { opts, item := AddOpts{}, "" @@ -354,15 +349,11 @@ func TestFuzzPriorityQueue(t *testing.T) { } }() } - }() + }) } for range 100 { - wg.Add(1) - - go func() { - defer wg.Done() - + wg.Go(func() { for { item, cont := func() (string, bool) { inQueueLock.Lock() @@ -411,7 +402,7 @@ func TestFuzzPriorityQueue(t *testing.T) { q.Done(item) }() } - }() + }) } wg.Wait() diff --git a/pkg/envtest/komega/equalobject.go b/pkg/envtest/komega/equalobject.go index a931c2718a..0295d204ab 100644 --- a/pkg/envtest/komega/equalobject.go +++ b/pkg/envtest/komega/equalobject.go @@ -74,7 +74,7 @@ func EqualObject(original runtime.Object, opts ...EqualObjectOption) types.Gomeg // Match compares the current object to the passed object and returns true if the objects are the same according to // the Matcher and MatchOptions. -func (m *equalObjectMatcher) Match(actual interface{}) (success bool, err error) { +func (m *equalObjectMatcher) Match(actual any) (success bool, err error) { // Nil checks required first here for: // 1) Nil equality which returns true // 2) One object nil which returns an error @@ -93,13 +93,13 @@ func (m *equalObjectMatcher) Match(actual interface{}) (success bool, err error) } // FailureMessage returns a message comparing the full objects after an unexpected failure to match has occurred. -func (m *equalObjectMatcher) FailureMessage(actual interface{}) (message string) { +func (m *equalObjectMatcher) FailureMessage(actual any) (message string) { return fmt.Sprintf("the following fields were expected to match but did not:\n%v\n%s", m.diffPaths, format.Message(actual, "expected to match", m.original)) } // NegatedFailureMessage returns a string stating that all fields matched, even though that was not expected. -func (m *equalObjectMatcher) NegatedFailureMessage(actual interface{}) (message string) { +func (m *equalObjectMatcher) NegatedFailureMessage(actual any) (message string) { return "it was expected that some fields do not match, but all of them did" } @@ -167,8 +167,8 @@ func (r *diffReporter) PopStep() { // calculateDiff calculates the difference between two objects and returns the // paths of the fields that do not match. -func (m *equalObjectMatcher) calculateDiff(actual interface{}) []diffPath { - var original interface{} = m.original +func (m *equalObjectMatcher) calculateDiff(actual any) []diffPath { + var original any = m.original // Remove the wrapping Object from unstructured.Unstructured to make comparison behave similar to // regular objects. if u, isUnstructured := actual.(runtime.Unstructured); isUnstructured { diff --git a/pkg/envtest/komega/equalobject_test.go b/pkg/envtest/komega/equalobject_test.go index 9fe10d1779..6163610a41 100644 --- a/pkg/envtest/komega/equalobject_test.go +++ b/pkg/envtest/komega/equalobject_test.go @@ -132,16 +132,16 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "works with unstructured.Unstructured", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ "name": "something", "namespace": "test", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ "name": "somethingelse", "namespace": "test", }, @@ -159,15 +159,15 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Equal field (spec) both in original and in modified", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "foo": "bar", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "foo": "bar", }, }, @@ -178,10 +178,10 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Equal nested field both in original and in modified", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ "A": "A", }, }, @@ -189,10 +189,10 @@ func TestEqualObjectMatcher(t *testing.T) { }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ "A": "A", }, }, @@ -206,15 +206,15 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Unequal field both in original and in modified", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "foo": "bar-changed", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "foo": "bar", }, }, @@ -224,10 +224,10 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Unequal nested field both in original and modified", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ "A": "A-Changed", }, }, @@ -235,10 +235,10 @@ func TestEqualObjectMatcher(t *testing.T) { }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ "A": "A", }, }, @@ -251,8 +251,8 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Value of type map with different values", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "map": map[string]string{ "A": "A-changed", "B": "B", @@ -262,8 +262,8 @@ func TestEqualObjectMatcher(t *testing.T) { }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "map": map[string]string{ "A": "A", // B missing @@ -278,8 +278,8 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Value of type Array or Slice with same length but different values", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "slice": []string{ "D", "C", @@ -289,8 +289,8 @@ func TestEqualObjectMatcher(t *testing.T) { }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "slice": []string{ "A", "B", @@ -306,22 +306,22 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Creation timestamp set to empty value on both original and modified", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", }, - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "selfLink": "foo", "creationTimestamp": metav1.Time{}, }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", }, - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "selfLink": "foo", "creationTimestamp": metav1.Time{}, }, @@ -334,11 +334,11 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Field only in modified", original: &unstructured.Unstructured{ - Object: map[string]interface{}{}, + Object: map[string]any{}, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "foo": "bar", }, }, @@ -348,13 +348,13 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Nested field only in modified", original: &unstructured.Unstructured{ - Object: map[string]interface{}{}, + Object: map[string]any{}, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ "A": "A", }, }, @@ -366,18 +366,18 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Creation timestamp exists on modified but not on original", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", }, - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "selfLink": "foo", "creationTimestamp": "2021-11-03T11:05:17Z", }, @@ -390,24 +390,24 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Field only in original", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "foo": "bar", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{}, + Object: map[string]any{}, }, want: false, }, { name: "Nested field only in original", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ "A": "A", }, }, @@ -415,26 +415,26 @@ func TestEqualObjectMatcher(t *testing.T) { }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{}, + Object: map[string]any{}, }, want: false, }, { name: "Creation timestamp exists on original but not on modified", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", }, - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "selfLink": "foo", "creationTimestamp": "2021-11-03T11:05:17Z", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", }, }, @@ -447,18 +447,18 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Unequal Metadata fields computed by the system or in status", original: &unstructured.Unstructured{ - Object: map[string]interface{}{}, + Object: map[string]any{}, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ "selfLink": "foo", "uid": "foo", "resourceVersion": "foo", "generation": "foo", "managedFields": "foo", }, - "status": map[string]interface{}{ + "status": map[string]any{ "foo": "bar", }, }, @@ -468,15 +468,15 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Unequal labels and annotations", original: &unstructured.Unstructured{ - Object: map[string]interface{}{}, + Object: map[string]any{}, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ + "labels": map[string]any{ "foo": "bar", }, - "annotations": map[string]interface{}{ + "annotations": map[string]any{ "foo": "bar", }, }, @@ -489,15 +489,15 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Unequal metadata fields ignored by IgnorePaths MatchOption", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ "name": "test", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ "name": "test", "selfLink": "foo", "uid": "foo", @@ -513,20 +513,20 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Unequal labels and annotations ignored by IgnorePaths MatchOption", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ "name": "test", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ "name": "test", - "labels": map[string]interface{}{ + "labels": map[string]any{ "foo": "bar", }, - "annotations": map[string]interface{}{ + "annotations": map[string]any{ "foo": "bar", }, }, @@ -538,14 +538,14 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Ignore fields are not compared", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{}, + Object: map[string]any{ + "spec": map[string]any{}, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "controlPlaneEndpoint": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ + "controlPlaneEndpoint": map[string]any{ "host": "", "port": 0, }, @@ -558,16 +558,16 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Not-ignored fields are still compared", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "annotations": map[string]interface{}{}, + Object: map[string]any{ + "metadata": map[string]any{ + "annotations": map[string]any{}, }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "annotations": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ + "annotations": map[string]any{ "ignored": "somevalue", "superflous": "shouldcausefailure", }, @@ -582,18 +582,18 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Unequal metadata fields not compared by setting MatchPaths MatchOption", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", }, }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", }, - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "selfLink": "foo", "uid": "foo", }, @@ -607,8 +607,8 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "No changes", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", "B": "B", "C": "C", // C only in original @@ -616,8 +616,8 @@ func TestEqualObjectMatcher(t *testing.T) { }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", "B": "B", }, @@ -628,8 +628,8 @@ func TestEqualObjectMatcher(t *testing.T) { { name: "Many changes", original: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", // B missing "C": "C", // C only in original @@ -637,8 +637,8 @@ func TestEqualObjectMatcher(t *testing.T) { }, }, modified: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ + Object: map[string]any{ + "spec": map[string]any{ "A": "A", "B": "B", }, diff --git a/pkg/handler/eventhandler_test.go b/pkg/handler/eventhandler_test.go index 2a7453f761..2023156146 100644 --- a/pkg/handler/eventhandler_test.go +++ b/pkg/handler/eventhandler_test.go @@ -183,7 +183,7 @@ var _ = Describe("Eventhandler", func() { i1, _ := q.Get() i2, _ := q.Get() - Expect([]interface{}{i1, i2}).To(ConsistOf( + Expect([]any{i1, i2}).To(ConsistOf( reconcile.Request{ NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}, reconcile.Request{ @@ -215,7 +215,7 @@ var _ = Describe("Eventhandler", func() { i1, _ := q.Get() i2, _ := q.Get() - Expect([]interface{}{i1, i2}).To(ConsistOf( + Expect([]any{i1, i2}).To(ConsistOf( reconcile.Request{ NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}, reconcile.Request{ @@ -280,7 +280,7 @@ var _ = Describe("Eventhandler", func() { i1, _ := q.Get() i2, _ := q.Get() - Expect([]interface{}{i1, i2}).To(ConsistOf( + Expect([]any{i1, i2}).To(ConsistOf( reconcile.Request{ NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}}, reconcile.Request{ @@ -362,7 +362,7 @@ var _ = Describe("Eventhandler", func() { i1, _ := q.Get() i2, _ := q.Get() - Expect([]interface{}{i1, i2}).To(ConsistOf( + Expect([]any{i1, i2}).To(ConsistOf( reconcile.Request{ NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo1-parent"}}, reconcile.Request{ @@ -602,7 +602,7 @@ var _ = Describe("Eventhandler", func() { i1, _ := q.Get() i2, _ := q.Get() i3, _ := q.Get() - Expect([]interface{}{i1, i2, i3}).To(ConsistOf( + Expect([]any{i1, i2, i3}).To(ConsistOf( reconcile.Request{ NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "foo1-parent"}}, reconcile.Request{ diff --git a/pkg/internal/controller/controller_test.go b/pkg/internal/controller/controller_test.go index 259f6669e2..1cef9cc602 100644 --- a/pkg/internal/controller/controller_test.go +++ b/pkg/internal/controller/controller_test.go @@ -636,14 +636,12 @@ var _ = Describe("controller", func() { By("Calling startEventSourcesAndQueueLocked multiple times in parallel") var wg sync.WaitGroup - for i := 1; i <= 5; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range 5 { + wg.Go(func() { err := ctrl.startEventSourcesAndQueueLocked(ctx) // All calls should return the same nil error Expect(err).NotTo(HaveOccurred()) - }() + }) } wg.Wait() @@ -1625,7 +1623,7 @@ var _ = Describe("controller", func() { } var wg sync.WaitGroup - for i := 0; i < 5; i++ { + for range 5 { wg.Add(1) go func() { defer GinkgoRecover() diff --git a/pkg/internal/flock/flock_other.go b/pkg/internal/flock/flock_other.go index 069a5b3a2c..1def472197 100644 --- a/pkg/internal/flock/flock_other.go +++ b/pkg/internal/flock/flock_other.go @@ -1,4 +1,4 @@ -// +build !linux,!darwin,!freebsd,!openbsd,!netbsd,!dragonfly +//go:build !linux && !darwin && !freebsd && !openbsd && !netbsd && !dragonfly /* Copyright 2016 The Kubernetes Authors. diff --git a/pkg/internal/flock/flock_unix.go b/pkg/internal/flock/flock_unix.go index 71ec576df2..be2a8c2cfd 100644 --- a/pkg/internal/flock/flock_unix.go +++ b/pkg/internal/flock/flock_unix.go @@ -1,5 +1,4 @@ //go:build linux || darwin || freebsd || openbsd || netbsd || dragonfly -// +build linux darwin freebsd openbsd netbsd dragonfly /* Copyright 2016 The Kubernetes Authors. diff --git a/pkg/internal/recorder/recorder_integration_test.go b/pkg/internal/recorder/recorder_integration_test.go index 061070166c..72c8335cf7 100644 --- a/pkg/internal/recorder/recorder_integration_test.go +++ b/pkg/internal/recorder/recorder_integration_test.go @@ -45,14 +45,14 @@ var _ = Describe("recorder", func() { Expect(err).NotTo(HaveOccurred()) By("Creating the Controller") - deprecatedRecorder := cm.GetEventRecorderFor("test-deprecated-recorder") //nolint:staticcheck + deprecatedRecorder := cm.GetEventRecorder("test-deprecated-recorder") recorder := cm.GetEventRecorder("test-recorder") instance, err := controller.New("foo-controller", cm, controller.Options{ Reconciler: reconcile.Func( func(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { dp, err := clientset.AppsV1().Deployments(request.Namespace).Get(ctx, request.Name, metav1.GetOptions{}) Expect(err).NotTo(HaveOccurred()) - deprecatedRecorder.Event(dp, corev1.EventTypeNormal, "deprecated-test-reason", "deprecated-test-msg") + deprecatedRecorder.Eventf(dp, nil, corev1.EventTypeNormal, "deprecated-test-reason", "deprecatedAction", "deprecated-test-msg") recorder.Eventf(dp, nil, corev1.EventTypeNormal, "test-reason", "test-action", "test-note") return reconcile.Result{}, nil }), diff --git a/pkg/internal/source/event_handler.go b/pkg/internal/source/event_handler.go index 7cc8c51555..9d614f34a5 100644 --- a/pkg/internal/source/event_handler.go +++ b/pkg/internal/source/event_handler.go @@ -60,7 +60,7 @@ type EventHandler[object client.Object, request comparable] struct { } // OnAdd creates CreateEvent and calls Create on EventHandler. -func (e *EventHandler[object, request]) OnAdd(obj interface{}, isInInitialList bool) { +func (e *EventHandler[object, request]) OnAdd(obj any, isInInitialList bool) { c := event.TypedCreateEvent[object]{ IsInInitialList: isInInitialList, } @@ -87,7 +87,7 @@ func (e *EventHandler[object, request]) OnAdd(obj interface{}, isInInitialList b } // OnUpdate creates UpdateEvent and calls Update on EventHandler. -func (e *EventHandler[object, request]) OnUpdate(oldObj, newObj interface{}) { +func (e *EventHandler[object, request]) OnUpdate(oldObj, newObj any) { u := event.TypedUpdateEvent[object]{} if o, ok := oldObj.(object); ok { @@ -120,7 +120,7 @@ func (e *EventHandler[object, request]) OnUpdate(oldObj, newObj interface{}) { } // OnDelete creates DeleteEvent and calls Delete on EventHandler. -func (e *EventHandler[object, request]) OnDelete(obj interface{}) { +func (e *EventHandler[object, request]) OnDelete(obj any) { d := event.TypedDeleteEvent[object]{} // Deal with tombstone events by pulling the object out. Tombstone events wrap the object in a diff --git a/pkg/internal/testing/addr/manager.go b/pkg/internal/testing/addr/manager.go index 2e2e41323a..e8f1f10d2a 100644 --- a/pkg/internal/testing/addr/manager.go +++ b/pkg/internal/testing/addr/manager.go @@ -126,7 +126,7 @@ func suggest(listenHost string) (*net.TCPListener, int, string, error) { // It makes sure that new port allocated does not conflict with old ports // allocated within 2 minute. func Suggest(listenHost string) (int, string, error) { - for i := 0; i < portConflictRetry; i++ { + for range portConflictRetry { listener, port, resolvedHost, err := suggest(listenHost) if err != nil { return -1, "", err diff --git a/pkg/internal/testing/certs/tinyca_test.go b/pkg/internal/testing/certs/tinyca_test.go index 9542975565..11207a2930 100644 --- a/pkg/internal/testing/certs/tinyca_test.go +++ b/pkg/internal/testing/certs/tinyca_test.go @@ -120,7 +120,7 @@ var _ = Describe("TinyCA", func() { localhostAddrs, err := net.LookupHost("localhost") Expect(err).NotTo(HaveOccurred(), "should be able to find IPs for localhost") - localhostIPs := make([]interface{}, len(localhostAddrs)) + localhostIPs := make([]any, len(localhostAddrs)) for i, addr := range localhostAddrs { // normalize the elements with To16 so we can compare them to the output of // of ParseIP safely (the alternative is a custom matcher that calls Equal, diff --git a/pkg/internal/testing/process/arguments.go b/pkg/internal/testing/process/arguments.go index 1e556e9980..caa417d2c2 100644 --- a/pkg/internal/testing/process/arguments.go +++ b/pkg/internal/testing/process/arguments.go @@ -26,7 +26,7 @@ import ( // RenderTemplates returns an []string to render the templates // // Deprecated: will be removed in favor of Arguments. -func RenderTemplates(argTemplates []string, data interface{}) (args []string, err error) { +func RenderTemplates(argTemplates []string, data any) (args []string, err error) { var t *template.Template for _, arg := range argTemplates { @@ -82,7 +82,7 @@ func SliceToArguments(sliceArgs []string, args *Arguments) []string { // Deprecated: will be removed when RenderTemplates is removed. type TemplateDefaults struct { // Data will be used to render the template. - Data interface{} + Data any // Defaults will be used to default structured arguments if no template is passed. Defaults map[string][]string // MinimalDefaults will be used to default structured arguments if a template is passed. diff --git a/pkg/internal/testing/process/procattr_other.go b/pkg/internal/testing/process/procattr_other.go index df13b341a4..e65ddc5f40 100644 --- a/pkg/internal/testing/process/procattr_other.go +++ b/pkg/internal/testing/process/procattr_other.go @@ -1,5 +1,4 @@ //go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !zos -// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!zos /* Copyright 2016 The Kubernetes Authors. diff --git a/pkg/internal/testing/process/procattr_unix.go b/pkg/internal/testing/process/procattr_unix.go index 83ad509af0..2bdf0c7c47 100644 --- a/pkg/internal/testing/process/procattr_unix.go +++ b/pkg/internal/testing/process/procattr_unix.go @@ -1,5 +1,4 @@ //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos /* Copyright 2023 The Kubernetes Authors. diff --git a/pkg/leaderelection/leader_election.go b/pkg/leaderelection/leader_election.go index 63d875b45a..7f59d82897 100644 --- a/pkg/leaderelection/leader_election.go +++ b/pkg/leaderelection/leader_election.go @@ -103,10 +103,7 @@ func NewResourceLock(config *rest.Config, recorderProvider recorder.Provider, op // RenewDeadline to keep a single hung request from forcing a leader loss. // Setting it to max(time.Second, RenewDeadline/2) as a reasonable heuristic. if options.RenewDeadline != 0 { - timeout := options.RenewDeadline / 2 - if timeout < time.Second { - timeout = time.Second - } + timeout := max(options.RenewDeadline/2, time.Second) config.Timeout = timeout } diff --git a/pkg/log/deleg.go b/pkg/log/deleg.go index 6eb551d3b6..948330b01d 100644 --- a/pkg/log/deleg.go +++ b/pkg/log/deleg.go @@ -30,7 +30,7 @@ type loggerPromise struct { promisesLock sync.Mutex name *string - tags []interface{} + tags []any } func (p *loggerPromise) WithName(l *delegatingLogSink, name string) *loggerPromise { @@ -47,7 +47,7 @@ func (p *loggerPromise) WithName(l *delegatingLogSink, name string) *loggerPromi } // WithValues provides a new Logger with the tags appended. -func (p *loggerPromise) WithValues(l *delegatingLogSink, tags ...interface{}) *loggerPromise { +func (p *loggerPromise) WithValues(l *delegatingLogSink, tags ...any) *loggerPromise { res := &loggerPromise{ logger: l, tags: tags, @@ -120,7 +120,7 @@ func (l *delegatingLogSink) Enabled(level int) bool { // the log line. The key/value pairs can then be used to add additional // variable information. The key/value pairs should alternate string // keys and arbitrary values. -func (l *delegatingLogSink) Info(level int, msg string, keysAndValues ...interface{}) { +func (l *delegatingLogSink) Info(level int, msg string, keysAndValues ...any) { eventuallyFulfillRoot() l.lock.RLock() defer l.lock.RUnlock() @@ -135,7 +135,7 @@ func (l *delegatingLogSink) Info(level int, msg string, keysAndValues ...interfa // The msg field should be used to add context to any underlying error, // while the err field should be used to attach the actual error that // triggered this log line, if present. -func (l *delegatingLogSink) Error(err error, msg string, keysAndValues ...interface{}) { +func (l *delegatingLogSink) Error(err error, msg string, keysAndValues ...any) { eventuallyFulfillRoot() l.lock.RLock() defer l.lock.RUnlock() @@ -164,7 +164,7 @@ func (l *delegatingLogSink) WithName(name string) logr.LogSink { } // WithValues provides a new Logger with the tags appended. -func (l *delegatingLogSink) WithValues(tags ...interface{}) logr.LogSink { +func (l *delegatingLogSink) WithValues(tags ...any) logr.LogSink { eventuallyFulfillRoot() l.lock.RLock() defer l.lock.RUnlock() diff --git a/pkg/log/log.go b/pkg/log/log.go index ade21d6fb5..48a4d490d3 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -88,7 +88,7 @@ var ( ) // FromContext returns a logger with predefined values from a context.Context. -func FromContext(ctx context.Context, keysAndValues ...interface{}) logr.Logger { +func FromContext(ctx context.Context, keysAndValues ...any) logr.Logger { log := Log if ctx != nil { if logger, err := logr.FromContext(ctx); err == nil { diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go index 404a859e4d..f56aef3706 100644 --- a/pkg/log/log_test.go +++ b/pkg/log/log_test.go @@ -29,7 +29,7 @@ var _ logr.LogSink = &delegatingLogSink{} // logInfo is the information for a particular fakeLogger message. type logInfo struct { name []string - tags []interface{} + tags []any msg string } @@ -43,7 +43,7 @@ type fakeLoggerRoot struct { // just records the name. type fakeLogger struct { name []string - tags []interface{} + tags []any root *fakeLoggerRoot } @@ -61,8 +61,8 @@ func (f *fakeLogger) WithName(name string) logr.LogSink { } } -func (f *fakeLogger) WithValues(vals ...interface{}) logr.LogSink { - tags := append([]interface{}(nil), f.tags...) +func (f *fakeLogger) WithValues(vals ...any) logr.LogSink { + tags := append([]any(nil), f.tags...) tags = append(tags, vals...) return &fakeLogger{ name: f.name, @@ -71,8 +71,8 @@ func (f *fakeLogger) WithValues(vals ...interface{}) logr.LogSink { } } -func (f *fakeLogger) Error(err error, msg string, vals ...interface{}) { - tags := append([]interface{}(nil), f.tags...) +func (f *fakeLogger) Error(err error, msg string, vals ...any) { + tags := append([]any(nil), f.tags...) tags = append(tags, "error", err) tags = append(tags, vals...) f.root.messages = append(f.root.messages, logInfo{ @@ -82,8 +82,8 @@ func (f *fakeLogger) Error(err error, msg string, vals ...interface{}) { }) } -func (f *fakeLogger) Info(level int, msg string, vals ...interface{}) { - tags := append([]interface{}(nil), f.tags...) +func (f *fakeLogger) Info(level int, msg string, vals ...any) { + tags := append([]any(nil), f.tags...) tags = append(tags, vals...) f.root.messages = append(f.root.messages, logInfo{ name: append([]string(nil), f.name...), @@ -113,8 +113,8 @@ var _ = Describe("logging", func() { By("ensuring that messages after the logger was set were logged") Expect(logger.root.messages).To(ConsistOf( - logInfo{name: []string{"runtimeLog"}, tags: []interface{}{"newtag", "newvalue1"}, msg: "after msg 1"}, - logInfo{name: []string{"runtimeLog"}, tags: []interface{}{"newtag", "newvalue2"}, msg: "after msg 2"}, + logInfo{name: []string{"runtimeLog"}, tags: []any{"newtag", "newvalue1"}, msg: "after msg 1"}, + logInfo{name: []string{"runtimeLog"}, tags: []any{"newtag", "newvalue2"}, msg: "after msg 2"}, )) }) }) @@ -259,10 +259,10 @@ var _ = Describe("logging", func() { By("ensuring that the messages are appropriately named") Expect(root.messages).To(ConsistOf( - logInfo{tags: []interface{}{"tag1", "val1"}, msg: "after 1"}, - logInfo{tags: []interface{}{"tag1", "val1", "tag2", "val2"}, msg: "after 2"}, - logInfo{tags: []interface{}{"tag1", "val1", "tag3", "val3"}, msg: "after 3"}, - logInfo{tags: []interface{}{"tag3", "val3"}, msg: "after 4"}, + logInfo{tags: []any{"tag1", "val1"}, msg: "after 1"}, + logInfo{tags: []any{"tag1", "val1", "tag2", "val2"}, msg: "after 2"}, + logInfo{tags: []any{"tag1", "val1", "tag3", "val3"}, msg: "after 3"}, + logInfo{tags: []any{"tag3", "val3"}, msg: "after 4"}, )) }) @@ -329,7 +329,7 @@ var _ = Describe("logging", func() { gotLog.Info("test message") Expect(root.messages).To(ConsistOf( - logInfo{name: []string{"my-logger"}, tags: []interface{}{"tag1", "value1"}, msg: "test message"}, + logInfo{name: []string{"my-logger"}, tags: []any{"tag1", "value1"}, msg: "test message"}, )) }) }) diff --git a/pkg/log/null.go b/pkg/log/null.go index f3e81074fe..f8dd84ca65 100644 --- a/pkg/log/null.go +++ b/pkg/log/null.go @@ -34,7 +34,7 @@ func (log NullLogSink) Init(logr.RuntimeInfo) { } // Info implements logr.InfoLogger. -func (NullLogSink) Info(_ int, _ string, _ ...interface{}) { +func (NullLogSink) Info(_ int, _ string, _ ...any) { // Do nothing. } @@ -44,7 +44,7 @@ func (NullLogSink) Enabled(level int) bool { } // Error implements logr.Logger. -func (NullLogSink) Error(_ error, _ string, _ ...interface{}) { +func (NullLogSink) Error(_ error, _ string, _ ...any) { // Do nothing. } @@ -54,6 +54,6 @@ func (log NullLogSink) WithName(_ string) logr.LogSink { } // WithValues implements logr.Logger. -func (log NullLogSink) WithValues(_ ...interface{}) logr.LogSink { +func (log NullLogSink) WithValues(_ ...any) logr.LogSink { return log } diff --git a/pkg/log/zap/zap_test.go b/pkg/log/zap/zap_test.go index 3e80113a65..1ff5e902d9 100644 --- a/pkg/log/zap/zap_test.go +++ b/pkg/log/zap/zap_test.go @@ -54,7 +54,7 @@ func (w *fakeSyncWriter) Sync() error { // logInfo is the information for a particular fakeLogger message. type logInfo struct { name []string - tags []interface{} + tags []any msg string } @@ -70,7 +70,7 @@ var _ logr.LogSink = &fakeLogger{} // just records the name. type fakeLogger struct { name []string - tags []interface{} + tags []any root *fakeLoggerRoot } @@ -88,8 +88,8 @@ func (f *fakeLogger) WithName(name string) logr.LogSink { } } -func (f *fakeLogger) WithValues(vals ...interface{}) logr.LogSink { - tags := append([]interface{}(nil), f.tags...) +func (f *fakeLogger) WithValues(vals ...any) logr.LogSink { + tags := append([]any(nil), f.tags...) tags = append(tags, vals...) return &fakeLogger{ name: f.name, @@ -98,8 +98,8 @@ func (f *fakeLogger) WithValues(vals ...interface{}) logr.LogSink { } } -func (f *fakeLogger) Error(err error, msg string, vals ...interface{}) { - tags := append([]interface{}(nil), f.tags...) +func (f *fakeLogger) Error(err error, msg string, vals ...any) { + tags := append([]any(nil), f.tags...) tags = append(tags, "error", err) tags = append(tags, vals...) f.root.messages = append(f.root.messages, logInfo{ @@ -109,8 +109,8 @@ func (f *fakeLogger) Error(err error, msg string, vals ...interface{}) { }) } -func (f *fakeLogger) Info(level int, msg string, vals ...interface{}) { - tags := append([]interface{}(nil), f.tags...) +func (f *fakeLogger) Info(level int, msg string, vals ...any) { + tags := append([]any(nil), f.tags...) tags = append(tags, vals...) f.root.messages = append(f.root.messages, logInfo{ name: append([]string(nil), f.name...), @@ -159,10 +159,10 @@ var _ = Describe("Zap logger setup", func() { logger.Info("here's a kubernetes object", "thing", pod) outRaw := logOut.Bytes() - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) - Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + Expect(res).To(HaveKeyWithValue("thing", map[string]any{ "name": pod.Name, "namespace": pod.Namespace, })) @@ -171,7 +171,7 @@ var _ = Describe("Zap logger setup", func() { It("should work fine with normal stringers", func() { logger.Info("here's a non-kubernetes stringer", "thing", testStringer{}) outRaw := logOut.Bytes() - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) Expect(res).To(HaveKeyWithValue("thing", "value")) @@ -183,10 +183,10 @@ var _ = Describe("Zap logger setup", func() { logger.Info("here's a kubernetes object", "thing", node) outRaw := logOut.Bytes() - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) - Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + Expect(res).To(HaveKeyWithValue("thing", map[string]any{ "name": node.Name, })) }) @@ -199,10 +199,10 @@ var _ = Describe("Zap logger setup", func() { logger.Info("here's a kubernetes object", "thing", node) outRaw := logOut.Bytes() - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) - Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + Expect(res).To(HaveKeyWithValue("thing", map[string]any{ "name": node.Name, "apiVersion": "v1", "kind": "Node", @@ -214,18 +214,18 @@ var _ = Describe("Zap logger setup", func() { logger.Info("here's a kubernetes object", "thing", name) outRaw := logOut.Bytes() - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) - Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + Expect(res).To(HaveKeyWithValue("thing", map[string]any{ "name": name.Name, })) }) It("should log an unstructured Kubernetes object", func() { pod := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ + Object: map[string]any{ + "metadata": map[string]any{ "name": "some-pod", "namespace": "some-ns", }, @@ -234,10 +234,10 @@ var _ = Describe("Zap logger setup", func() { logger.Info("here's a kubernetes object", "thing", pod) outRaw := logOut.Bytes() - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) - Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + Expect(res).To(HaveKeyWithValue("thing", map[string]any{ "name": "some-pod", "namespace": "some-ns", })) @@ -248,10 +248,10 @@ var _ = Describe("Zap logger setup", func() { logger.Info("here's a kubernetes object", "thing", name) outRaw := logOut.Bytes() - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) - Expect(res).To(HaveKeyWithValue("thing", map[string]interface{}{ + Expect(res).To(HaveKeyWithValue("thing", map[string]any{ "name": name.Name, "namespace": name.Namespace, })) @@ -550,7 +550,7 @@ var _ = Describe("Zap log level flag options setup", func() { outRaw := logOut.Bytes() - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) Expect(res["ts"]).Should(MatchRegexp(iso8601Pattern)) }) @@ -569,7 +569,7 @@ var _ = Describe("Zap log level flag options setup", func() { log.Info("This is a test message") outRaw := logOut.Bytes() // Assert for JSON Encoder - res := map[string]interface{}{} + res := map[string]any{} Expect(json.Unmarshal(outRaw, &res)).To(Succeed()) // Assert for MessageKey Expect(string(outRaw)).Should(ContainSubstring("MillisTimeFormat")) diff --git a/pkg/manager/runnable_group_test.go b/pkg/manager/runnable_group_test.go index e22f2c00d5..9a278a1495 100644 --- a/pkg/manager/runnable_group_test.go +++ b/pkg/manager/runnable_group_test.go @@ -181,7 +181,7 @@ var _ = Describe("runnableGroup", func() { Expect(rg.Start(ctx)).To(Succeed()) }() - for i := 0; i < 20; i++ { + for i := range 20 { go func(i int) { defer GinkgoRecover() @@ -199,7 +199,7 @@ var _ = Describe("runnableGroup", func() { exited := ptr.To(int64(0)) rg := newRunnableGroup(defaultBaseContext, errCh) - for i := 0; i < 10; i++ { + for i := range 10 { Expect(rg.Add(RunnableFunc(func(c context.Context) error { defer atomic.AddInt64(exited, 1) <-ctx.Done() @@ -231,7 +231,7 @@ var _ = Describe("runnableGroup", func() { Expect(rg.Start(ctx)).To(Succeed()) }() - for i := 0; i < 20; i++ { + for i := range 20 { go func(i int) { defer GinkgoRecover() @@ -264,7 +264,7 @@ var _ = Describe("runnableGroup", func() { rg.StopAndWait(ctx) }() - for i := 0; i < 200; i++ { + for i := range 200 { go func(i int) { defer GinkgoRecover() @@ -293,7 +293,7 @@ var _ = Describe("runnableGroup", func() { Expect(rg.Start(ctx)).To(Succeed()) }() - for i := 0; i < 20; i++ { + for i := range 20 { go func(i int) { defer GinkgoRecover() diff --git a/pkg/manager/signals/signal_posix.go b/pkg/manager/signals/signal_posix.go index a0f00a7321..2b24faa428 100644 --- a/pkg/manager/signals/signal_posix.go +++ b/pkg/manager/signals/signal_posix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2017 The Kubernetes Authors. diff --git a/pkg/manager/signals/signal_test.go b/pkg/manager/signals/signal_test.go index 134937e012..f2e3c4d02d 100644 --- a/pkg/manager/signals/signal_test.go +++ b/pkg/manager/signals/signal_test.go @@ -69,7 +69,7 @@ func (t *Task) Run(c chan os.Signal) { } func handle() { - for i := 0; i < 5; i++ { + for range 5 { fmt.Print("#") time.Sleep(time.Millisecond * 100) } diff --git a/pkg/scheme/scheme_test.go b/pkg/scheme/scheme_test.go index 37c6766e6f..0f68fb0e2b 100644 --- a/pkg/scheme/scheme_test.go +++ b/pkg/scheme/scheme_test.go @@ -41,8 +41,8 @@ var _ = Describe("Scheme", func() { internalGv := schema.GroupVersion{Group: "core", Version: "__internal"} emptyGv := schema.GroupVersion{Group: "", Version: "v1"} Expect(s.AllKnownTypes()).To(MatchAllKeys(Keys{ - gv.WithKind("Pod"): Equal(reflect.TypeOf(corev1.Pod{})), - gv.WithKind("PodList"): Equal(reflect.TypeOf(corev1.PodList{})), + gv.WithKind("Pod"): Equal(reflect.TypeFor[corev1.Pod]()), + gv.WithKind("PodList"): Equal(reflect.TypeFor[corev1.PodList]()), // Base types gv.WithKind("CreateOptions"): Ignore(), @@ -80,12 +80,12 @@ var _ = Describe("Scheme", func() { emptyGv := schema.GroupVersion{Group: "", Version: "v1"} Expect(s.AllKnownTypes()).To(MatchAllKeys(Keys{ // Types from b1 - gv1.WithKind("Pod"): Equal(reflect.TypeOf(corev1.Pod{})), - gv1.WithKind("PodList"): Equal(reflect.TypeOf(corev1.PodList{})), + gv1.WithKind("Pod"): Equal(reflect.TypeFor[corev1.Pod]()), + gv1.WithKind("PodList"): Equal(reflect.TypeFor[corev1.PodList]()), // Types from b2 - gv2.WithKind("Deployment"): Equal(reflect.TypeOf(appsv1.Deployment{})), - gv2.WithKind("DeploymentList"): Equal(reflect.TypeOf(appsv1.DeploymentList{})), + gv2.WithKind("Deployment"): Equal(reflect.TypeFor[appsv1.Deployment]()), + gv2.WithKind("DeploymentList"): Equal(reflect.TypeFor[appsv1.DeploymentList]()), // Base types gv1.WithKind("CreateOptions"): Ignore(), diff --git a/pkg/source/source_integration_test.go b/pkg/source/source_integration_test.go index cc0ba530ec..afe39c3bcf 100644 --- a/pkg/source/source_integration_test.go +++ b/pkg/source/source_integration_test.go @@ -41,7 +41,7 @@ var _ = Describe("Source", func() { var instance1, instance2 source.Source var obj client.Object var q workqueue.TypedRateLimitingInterface[reconcile.Request] - var c1, c2 chan interface{} + var c1, c2 chan any var ns string count := 0 @@ -59,8 +59,8 @@ var _ = Describe("Source", func() { workqueue.TypedRateLimitingQueueConfig[reconcile.Request]{ Name: "test", }) - c1 = make(chan interface{}) - c2 = make(chan interface{}) + c1 = make(chan any) + c2 = make(chan any) }) AfterEach(func(ctx SpecContext) { @@ -101,7 +101,7 @@ var _ = Describe("Source", func() { } // Create an event handler to verify the events - newHandler := func(c chan interface{}) handler.Funcs { + newHandler := func(c chan any) handler.Funcs { return handler.Funcs{ CreateFunc: func(ctx context.Context, evt event.CreateEvent, rli workqueue.TypedRateLimitingInterface[reconcile.Request]) { defer GinkgoRecover() diff --git a/pkg/webhook/admission/decode.go b/pkg/webhook/admission/decode.go index 55f1cafb5e..576262cf70 100644 --- a/pkg/webhook/admission/decode.go +++ b/pkg/webhook/admission/decode.go @@ -79,7 +79,7 @@ func (d *decoder) DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) er } if unstructuredInto, isUnstructured := into.(runtime.Unstructured); isUnstructured { // unmarshal into unstructured's underlying object to avoid calling the decoder - var object map[string]interface{} + var object map[string]any if err := json.Unmarshal(rawObj.Raw, &object); err != nil { return err } diff --git a/pkg/webhook/admission/decode_test.go b/pkg/webhook/admission/decode_test.go index 130308800f..c8a8208f67 100644 --- a/pkg/webhook/admission/decode_test.go +++ b/pkg/webhook/admission/decode_test.go @@ -139,7 +139,7 @@ var _ = Describe("Admission Webhook Decoder", func() { Expect(decoder.Decode(req, &target)).To(Succeed()) By("sanity-checking the metadata on the output object") - Expect(target.Object["metadata"]).To(Equal(map[string]interface{}{ + Expect(target.Object["metadata"]).To(Equal(map[string]any{ "name": "foo", "namespace": "default", })) @@ -149,7 +149,7 @@ var _ = Describe("Admission Webhook Decoder", func() { Expect(decoder.DecodeRaw(req.Object, &target2)).To(Succeed()) By("sanity-checking the metadata on the output object") - Expect(target2.Object["metadata"]).To(Equal(map[string]interface{}{ + Expect(target2.Object["metadata"]).To(Equal(map[string]any{ "name": "foo", "namespace": "default", })) diff --git a/pkg/webhook/admission/defaulter_custom.go b/pkg/webhook/admission/defaulter_custom.go index 1dc8af10ee..d946966d4e 100644 --- a/pkg/webhook/admission/defaulter_custom.go +++ b/pkg/webhook/admission/defaulter_custom.go @@ -38,6 +38,7 @@ type Defaulter[T runtime.Object] interface { } // CustomDefaulter defines functions for setting defaults on resources. +// // Deprecated: CustomDefaulter is deprecated, use Defaulter instead type CustomDefaulter = Defaulter[runtime.Object] diff --git a/pkg/webhook/admission/validator_custom.go b/pkg/webhook/admission/validator_custom.go index abd68e88bf..f8401571d0 100644 --- a/pkg/webhook/admission/validator_custom.go +++ b/pkg/webhook/admission/validator_custom.go @@ -51,6 +51,7 @@ type Validator[T runtime.Object] interface { } // CustomValidator defines functions for validating an operation. +// // Deprecated: CustomValidator is deprecated, use Validator instead type CustomValidator = Validator[runtime.Object] @@ -73,6 +74,7 @@ func WithValidator[T runtime.Object](scheme *runtime.Scheme, validator Validator } // WithCustomValidator creates a new Webhook for a CustomValidator. +// // Deprecated: WithCustomValidator is deprecated, use WithValidator instead func WithCustomValidator(scheme *runtime.Scheme, obj runtime.Object, validator CustomValidator) *Webhook { return &Webhook{ diff --git a/pkg/webhook/alias.go b/pkg/webhook/alias.go index b4f16a3f5f..518d52f364 100644 --- a/pkg/webhook/alias.go +++ b/pkg/webhook/alias.go @@ -24,12 +24,14 @@ import ( // define some aliases for common bits of the webhook functionality // CustomDefaulter defines functions for setting defaults on resources. +// // Deprecated: Use admission.Defaulter instead. -type CustomDefaulter = admission.CustomDefaulter +type CustomDefaulter = admission.CustomDefaulter //nolint:staticcheck // CustomValidator defines functions for validating an operation. +// // Deprecated: Use admission.Validator instead. -type CustomValidator = admission.CustomValidator +type CustomValidator = admission.CustomValidator //nolint:staticcheck // AdmissionRequest defines the input for an admission handler. // It contains information to identify the object in diff --git a/tools/setup-envtest/env/exit.go b/tools/setup-envtest/env/exit.go index ae393b593b..640e65a4fc 100644 --- a/tools/setup-envtest/env/exit.go +++ b/tools/setup-envtest/env/exit.go @@ -12,7 +12,7 @@ import ( // Exit exits with the given code and error message. // // Defer HandleExitWithCode in main to catch this and get the right behavior. -func Exit(code int, msg string, args ...interface{}) { +func Exit(code int, msg string, args ...any) { panic(&exitCode{ code: code, err: fmt.Errorf(msg, args...), @@ -23,7 +23,7 @@ func Exit(code int, msg string, args ...interface{}) { // wrapping the underlying error passed as well. // // Defer HandleExitWithCode in main to catch this and get the right behavior. -func ExitCause(code int, err error, msg string, args ...interface{}) { +func ExitCause(code int, err error, msg string, args ...any) { args = append(args, err) panic(&exitCode{ code: code, @@ -48,7 +48,7 @@ func (c *exitCode) Unwrap() error { // asExit checks if the given (panic) value is an exitCode error, // and if so stores it in the given pointer. It's roughly analogous // to errors.As, except it works on recover() values. -func asExit(val interface{}, exit **exitCode) bool { +func asExit(val any, exit **exitCode) bool { if val == nil { return false } @@ -81,7 +81,7 @@ func HandleExitWithCode() { // the cause. // // It's mainly useful for testing, normally you'd use HandleExitWithCode. -func CheckRecover(cause interface{}, cb func(int, error)) bool { +func CheckRecover(cause any, cb func(int, error)) bool { if cause == nil { return false } From ea4566fe6033d3e306c9e372fac45b7d2696fe57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Puczyn=CC=81ski?= Date: Thu, 20 Nov 2025 00:39:13 +0100 Subject: [PATCH 49/68] Update client.Apply example --- pkg/client/example_test.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/client/example_test.go b/pkg/client/example_test.go index 390dc10143..12998d55de 100644 --- a/pkg/client/example_test.go +++ b/pkg/client/example_test.go @@ -25,7 +25,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" corev1ac "k8s.io/client-go/applyconfigurations/core/v1" @@ -218,10 +217,23 @@ func ExampleClient_patch() { func ExampleClient_apply() { // Using a typed object. configMap := corev1ac.ConfigMap("name", "namespace").WithData(map[string]string{"key": "value"}) - // c is a created client. - u := &unstructured.Unstructured{} - u.Object, _ = runtime.DefaultUnstructuredConverter.ToUnstructured(configMap) - _ = c.Patch(context.Background(), u, client.Apply, client.ForceOwnership, client.FieldOwner("field-owner")) + _ = c.Apply(context.Background(), configMap) + + // Using a unstructured object. + u := &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]any{ + "name": "name", + "namespace": "namespace", + }, + "data": map[string]any{ + "key": "value", + }, + }, + } + _ = c.Apply(context.Background(), client.ApplyConfigurationFromUnstructured(u)) } // This example shows how to use the client with typed and unstructured objects to patch objects' status. From 56f7b6493b03d3f5a9b6e0d291b71e253d6eb818 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Wed, 19 Nov 2025 20:45:36 -0500 Subject: [PATCH 50/68] :seedling: Bump k8s.io/* deps to 0.35.beta0 --- examples/scratch-env/go.mod | 8 ++++---- examples/scratch-env/go.sum | 16 ++++++++-------- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ tools/setup-envtest/go.mod | 2 +- tools/setup-envtest/go.sum | 4 ++-- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod index 19b7aec9e6..5051a1de4e 100644 --- a/examples/scratch-env/go.mod +++ b/examples/scratch-env/go.mod @@ -52,10 +52,10 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.0-alpha.3 // indirect - k8s.io/apiextensions-apiserver v0.35.0-alpha.3 // indirect - k8s.io/apimachinery v0.35.0-alpha.3 // indirect - k8s.io/client-go v0.35.0-alpha.3 // indirect + k8s.io/api v0.35.0-beta.0 // indirect + k8s.io/apiextensions-apiserver v0.35.0-beta.0 // indirect + k8s.io/apimachinery v0.35.0-beta.0 // indirect + k8s.io/client-go v0.35.0-beta.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum index e99c71699a..4a8322fbb3 100644 --- a/examples/scratch-env/go.sum +++ b/examples/scratch-env/go.sum @@ -146,14 +146,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.35.0-alpha.3 h1:BdcXkJ4n/NKhfg06PaSDG8r8Mpe9g3KO9Fkj7B/F8/4= -k8s.io/api v0.35.0-alpha.3/go.mod h1:SArWbUwVv7VhTGGbKX0RoMPXiT6ztjjzkKpRRdl6+E0= -k8s.io/apiextensions-apiserver v0.35.0-alpha.3 h1:Js9dTA0LVvR93tWOZtyJuZKMs0CQuaxNiaM1dwtUM0Q= -k8s.io/apiextensions-apiserver v0.35.0-alpha.3/go.mod h1:LWHywtk0D0qSiJd4Ql65tMY8hDKJYsgBg2jQijeZJNE= -k8s.io/apimachinery v0.35.0-alpha.3 h1:aHqVUsi78MIDmMfmMTRMAnpxUlA7poaU1iNXN/sM6gs= -k8s.io/apimachinery v0.35.0-alpha.3/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= -k8s.io/client-go v0.35.0-alpha.3 h1:F7XDcT1E02zv/BeD7Tt1hXJO2aZjIg/jqMZ/oz3yre4= -k8s.io/client-go v0.35.0-alpha.3/go.mod h1:+gl5b5GzUQycBhxcqoQ/dxyFqz4A3Sx9djuc3TckFN8= +k8s.io/api v0.35.0-beta.0 h1:eqAAVeSatXNnsPjaeFrFGqSl5ihtPY4e8Txy2nYPOnw= +k8s.io/api v0.35.0-beta.0/go.mod h1:UXuvkssy8lHPSP381eqqBOW4BvRTicVpRjv7k2sjo4Y= +k8s.io/apiextensions-apiserver v0.35.0-beta.0 h1:1e0ar0DsUPqR0G6RPHXGVe7G/+Grex+pUF8hXu5+OxE= +k8s.io/apiextensions-apiserver v0.35.0-beta.0/go.mod h1:/UUhEsqEZ5q4TZzGFvAf4V/x00lyryOiLJsL5oD9BGM= +k8s.io/apimachinery v0.35.0-beta.0 h1:vVoDiASLwUEv5yZceZCBRPXBc1f9wUOZs7ZbEbGr5sY= +k8s.io/apimachinery v0.35.0-beta.0/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= +k8s.io/client-go v0.35.0-beta.0 h1:4APvMU7+XwWF+XoqAv+gbtSmwjPCXXXo4XVcY89Rde0= +k8s.io/client-go v0.35.0-beta.0/go.mod h1:+XxnPEoaCIB5G0zpwXRh3AnT+CvgS5lA+AFr9EtHUcA= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/go.mod b/go.mod index d403a378bb..e545f60984 100644 --- a/go.mod +++ b/go.mod @@ -21,11 +21,11 @@ require ( golang.org/x/sys v0.37.0 gomodules.xyz/jsonpatch/v2 v2.4.0 gopkg.in/evanphx/json-patch.v4 v4.13.0 // Using v4 to match upstream - k8s.io/api v0.35.0-alpha.3 - k8s.io/apiextensions-apiserver v0.35.0-alpha.3 - k8s.io/apimachinery v0.35.0-alpha.3 - k8s.io/apiserver v0.35.0-alpha.3 - k8s.io/client-go v0.35.0-alpha.3 + k8s.io/api v0.35.0-beta.0 + k8s.io/apiextensions-apiserver v0.35.0-beta.0 + k8s.io/apimachinery v0.35.0-beta.0 + k8s.io/apiserver v0.35.0-beta.0 + k8s.io/client-go v0.35.0-beta.0 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/structured-merge-diff/v6 v6.3.0 @@ -94,7 +94,7 @@ require ( google.golang.org/protobuf v1.36.8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/component-base v0.35.0-alpha.3 // indirect + k8s.io/component-base v0.35.0-beta.0 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index a044979c4d..27b1b405ea 100644 --- a/go.sum +++ b/go.sum @@ -223,18 +223,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.35.0-alpha.3 h1:BdcXkJ4n/NKhfg06PaSDG8r8Mpe9g3KO9Fkj7B/F8/4= -k8s.io/api v0.35.0-alpha.3/go.mod h1:SArWbUwVv7VhTGGbKX0RoMPXiT6ztjjzkKpRRdl6+E0= -k8s.io/apiextensions-apiserver v0.35.0-alpha.3 h1:Js9dTA0LVvR93tWOZtyJuZKMs0CQuaxNiaM1dwtUM0Q= -k8s.io/apiextensions-apiserver v0.35.0-alpha.3/go.mod h1:LWHywtk0D0qSiJd4Ql65tMY8hDKJYsgBg2jQijeZJNE= -k8s.io/apimachinery v0.35.0-alpha.3 h1:aHqVUsi78MIDmMfmMTRMAnpxUlA7poaU1iNXN/sM6gs= -k8s.io/apimachinery v0.35.0-alpha.3/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= -k8s.io/apiserver v0.35.0-alpha.3 h1:OsYEVXKQgIErQdEO1CSnytsIypIrwdL0kXHaIIPMYso= -k8s.io/apiserver v0.35.0-alpha.3/go.mod h1:w3JHMk6fuQ6QUkWfUoXnurNlbJHleRC8Bk9yE3gWHn8= -k8s.io/client-go v0.35.0-alpha.3 h1:F7XDcT1E02zv/BeD7Tt1hXJO2aZjIg/jqMZ/oz3yre4= -k8s.io/client-go v0.35.0-alpha.3/go.mod h1:+gl5b5GzUQycBhxcqoQ/dxyFqz4A3Sx9djuc3TckFN8= -k8s.io/component-base v0.35.0-alpha.3 h1:1ldfRd8A5qYvjZipXvXdXhBxI8fFN7MYRAHTIrc26T8= -k8s.io/component-base v0.35.0-alpha.3/go.mod h1:DDQa1Mchpy4/+kz0TVlMxSBZyddpnzTHka+N3BnXL3E= +k8s.io/api v0.35.0-beta.0 h1:eqAAVeSatXNnsPjaeFrFGqSl5ihtPY4e8Txy2nYPOnw= +k8s.io/api v0.35.0-beta.0/go.mod h1:UXuvkssy8lHPSP381eqqBOW4BvRTicVpRjv7k2sjo4Y= +k8s.io/apiextensions-apiserver v0.35.0-beta.0 h1:1e0ar0DsUPqR0G6RPHXGVe7G/+Grex+pUF8hXu5+OxE= +k8s.io/apiextensions-apiserver v0.35.0-beta.0/go.mod h1:/UUhEsqEZ5q4TZzGFvAf4V/x00lyryOiLJsL5oD9BGM= +k8s.io/apimachinery v0.35.0-beta.0 h1:vVoDiASLwUEv5yZceZCBRPXBc1f9wUOZs7ZbEbGr5sY= +k8s.io/apimachinery v0.35.0-beta.0/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= +k8s.io/apiserver v0.35.0-beta.0 h1:V0daLhmZy3hqohfz1pDImWH+Js4mI02E5Sv2Zin96f4= +k8s.io/apiserver v0.35.0-beta.0/go.mod h1:XvMSZG0iw7xUDsaQIHf36L0TomhYAbgCCc4TeLgkovU= +k8s.io/client-go v0.35.0-beta.0 h1:4APvMU7+XwWF+XoqAv+gbtSmwjPCXXXo4XVcY89Rde0= +k8s.io/client-go v0.35.0-beta.0/go.mod h1:+XxnPEoaCIB5G0zpwXRh3AnT+CvgS5lA+AFr9EtHUcA= +k8s.io/component-base v0.35.0-beta.0 h1:zqaQLtIs5VDBNsg4A/1Nkq2pC7fQhcgcvwHRgI7utFE= +k8s.io/component-base v0.35.0-beta.0/go.mod h1:gkWiSIt+PGyxlzWzy/8PGqKvKsLK6mujscMs+Qjzgn4= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/tools/setup-envtest/go.mod b/tools/setup-envtest/go.mod index 9b88b1487c..dcccfe130f 100644 --- a/tools/setup-envtest/go.mod +++ b/tools/setup-envtest/go.mod @@ -10,7 +10,7 @@ require ( github.com/spf13/afero v1.12.0 github.com/spf13/pflag v1.0.9 go.uber.org/zap v1.27.0 - k8s.io/apimachinery v0.35.0-alpha.3 + k8s.io/apimachinery v0.35.0-beta.0 sigs.k8s.io/yaml v1.6.0 ) diff --git a/tools/setup-envtest/go.sum b/tools/setup-envtest/go.sum index 7bcb4a2fee..1769902891 100644 --- a/tools/setup-envtest/go.sum +++ b/tools/setup-envtest/go.sum @@ -81,7 +81,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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.35.0-alpha.3 h1:aHqVUsi78MIDmMfmMTRMAnpxUlA7poaU1iNXN/sM6gs= -k8s.io/apimachinery v0.35.0-alpha.3/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= +k8s.io/apimachinery v0.35.0-beta.0 h1:vVoDiASLwUEv5yZceZCBRPXBc1f9wUOZs7ZbEbGr5sY= +k8s.io/apimachinery v0.35.0-beta.0/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 94aa0e5f1c85d77aaf6e07df14a2f4145a17fa38 Mon Sep 17 00:00:00 2001 From: Saketh Kalaga <51327242+renormalize@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:41:46 +0530 Subject: [PATCH 51/68] Update `README.md`'s compatibility matrix for `v0.22.x`. Signed-off-by: Saketh Kalaga <51327242+renormalize@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 54bacad42e..8549f4e880 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Compatible k8s.io/*, client-go and minimum Go versions can be looked up in our [ | | k8s.io/*, client-go | minimum Go version | |----------|:-------------------:|:------------------:| +| CR v0.22 | v0.34 | 1.24 | | CR v0.21 | v0.33 | 1.24 | | CR v0.20 | v0.32 | 1.23 | | CR v0.19 | v0.31 | 1.22 | From 3893e04629c6c4e0dc9639fc5da37c1e7436ae55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Puczy=C5=84ski?= Date: Fri, 21 Nov 2025 14:34:34 +0100 Subject: [PATCH 52/68] =?UTF-8?q?=E2=9C=A8=20Add=20FieldOwner=20field=20to?= =?UTF-8?q?=20client.Options=20(#3389)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add FieldOwner field to client.Options * Update client.Options.FieldOwner documentation --- pkg/client/client.go | 13 +++++++++++++ pkg/client/client_test.go | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/pkg/client/client.go b/pkg/client/client.go index 39050de457..1a14fcc5e2 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -52,6 +52,15 @@ type Options struct { // DryRun instructs the client to only perform dry run requests. DryRun *bool + + // FieldOwner, if provided, sets the default field manager for all write operations + // (Create, Update, Patch, Apply) performed by this client. The field manager is used by + // the server for Server-Side Apply to track field ownership. + // For more details, see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#field-management + // + // This default can be overridden for a specific call by passing a [FieldOwner] option + // to the method. + FieldOwner string } // CacheOptions are options for creating a cache-backed client. @@ -99,6 +108,10 @@ func New(config *rest.Config, options Options) (c Client, err error) { if err == nil && options.DryRun != nil && *options.DryRun { c = NewDryRunClient(c) } + if fo := options.FieldOwner; fo != "" { + c = WithFieldOwner(c, fo) + } + return c, err } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 593a2b3c18..d52f043692 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -365,6 +365,19 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(cl.List(ctx, &corev1.NamespaceList{})).To(Succeed()) Expect(cache.Called).To(Equal(0)) }) + + It("should use the provided FieldOwner if provided", func(ctx SpecContext) { + cl, err := client.New(cfg, client.Options{FieldOwner: "test-owner"}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + // no explicit FieldOwner option set on Apply method call + err = cl.Apply(ctx, corev1applyconfigurations.ConfigMap("test-cm", "default").WithData(map[string]string{"foo": "bar"})) + Expect(err).NotTo(HaveOccurred()) + actual, err := clientset.CoreV1().ConfigMaps("default").Get(ctx, "test-cm", metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + Expect(actual.ManagedFields).To(HaveLen(1)) + Expect(actual.ManagedFields[0].Manager).To(Equal("test-owner")) + }) }) Describe("Create", func() { From 9758f5c2ef1436ed558c79f144013bb001945071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Puczy=C5=84ski?= Date: Sun, 23 Nov 2025 18:06:35 +0100 Subject: [PATCH 53/68] =?UTF-8?q?=E2=9C=A8=20Add=20FieldValidation=20setti?= =?UTF-8?q?ng=20to=20client.Options=20(#3393)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add FieldValidation field to client.Options struct * improve new field documentation * Update pkg/client/client.go Co-authored-by: Alvaro Aleman * remove duplicate information in client.Options.FieldValidation documentation * Update pkg/client/client.go Co-authored-by: Alvaro Aleman --------- Co-authored-by: Alvaro Aleman --- pkg/client/client.go | 13 +++++++++++ pkg/client/client_test.go | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/pkg/client/client.go b/pkg/client/client.go index 1a14fcc5e2..ad946daeaa 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -61,6 +61,16 @@ type Options struct { // This default can be overridden for a specific call by passing a [FieldOwner] option // to the method. FieldOwner string + + // FieldValidation sets the field validation strategy for all mutating operations performed by this client + // and subresource clients created from it. + // The exception are apply requests which are always strict, regardless of the FieldValidation setting. + // Available values for this option can be found in "k8s.io/apimachinery/pkg/apis/meta/v1" package and are: + // - FieldValidationIgnore + // - FieldValidationWarn + // - FieldValidationStrict + // For more details, see: https://kubernetes.io/docs/reference/using-api/api-concepts/#field-validation + FieldValidation string } // CacheOptions are options for creating a cache-backed client. @@ -111,6 +121,9 @@ func New(config *rest.Config, options Options) (c Client, err error) { if fo := options.FieldOwner; fo != "" { c = WithFieldOwner(c, fo) } + if fv := options.FieldValidation; fv != "" { + c = WithFieldValidation(c, FieldValidation(fv)) + } return c, err } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index d52f043692..b2890c385d 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -43,6 +43,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1" autoscaling1applyconfigurations "k8s.io/client-go/applyconfigurations/autoscaling/v1" corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" @@ -378,6 +379,52 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.ManagedFields).To(HaveLen(1)) Expect(actual.ManagedFields[0].Manager).To(Equal("test-owner")) }) + + Context("with the FieldValidation option", func() { + It("should log warnings with FieldValidation equal to Warn", func(ctx SpecContext) { + restCfg := rest.CopyConfig(cfg) + var testLog bytes.Buffer + restCfg.WarningHandler = rest.NewWarningWriter(&testLog, rest.WarningWriterOptions{}) + + warnClient, err := client.New(restCfg, client.Options{FieldValidation: metav1.FieldValidationWarn}) + Expect(err).NotTo(HaveOccurred()) + Expect(warnClient).NotTo(BeNil()) + + unstrContent, err := runtime.DefaultUnstructuredConverter.ToUnstructured( + corev1applyconfigurations.ConfigMap("test-cm-"+rand.String(3), "default"). + WithData(map[string]string{"foo": "bar"}), + ) + Expect(err).NotTo(HaveOccurred()) + unstrContent["additionalField"] = "test" + cm := &unstructured.Unstructured{Object: unstrContent} + + err = warnClient.Create(ctx, cm) + Expect(err).NotTo(HaveOccurred()) + Expect(testLog.String()).To(ContainSubstring(`Warning: unknown field "additionalField"`)) + + }) + It("should fail write operation if FieldValidation equals Strict", func(ctx SpecContext) { + restCfg := rest.CopyConfig(cfg) + var testLog bytes.Buffer + restCfg.WarningHandler = rest.NewWarningWriter(&testLog, rest.WarningWriterOptions{}) + strictClient, err := client.New(restCfg, client.Options{FieldValidation: metav1.FieldValidationStrict}) + Expect(err).NotTo(HaveOccurred()) + + unstrContent, err := runtime.DefaultUnstructuredConverter.ToUnstructured( + corev1applyconfigurations.ConfigMap("test-cm-"+rand.String(3), "default"). + WithData(map[string]string{"foo": "bar"}), + ) + Expect(err).NotTo(HaveOccurred()) + unstrContent["additionalField"] = "test" + cm := &unstructured.Unstructured{Object: unstrContent} + + err = strictClient.Create(ctx, cm) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("unknown field \"additionalField\""))) + Expect(err).To(MatchError(ContainSubstring("strict decoding error"))) + Expect(testLog.String()).To(BeEmpty()) + }) + }) }) Describe("Create", func() { From 35dc828044897ceaffa585994af2c961e97b3b5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:06:15 +0000 Subject: [PATCH 54/68] :seedling: Bump the all-github-actions group with 3 updates Bumps the all-github-actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-go](https://github.com/actions/setup-go) and [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action). Updates `actions/checkout` from 5.0.1 to 6.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/93cb6efe18208431cddfb8368fd83d5badbf9bfd...1af3b93b6815bc44a9784bd300feb67ff0d1eeb3) Updates `actions/setup-go` from 6.0.0 to 6.1.0 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/44694675825211faa026b3c33043df3e48a5fa00...4dc6199c7b1a012772edbd06daecab0f50c9053c) Updates `golangci/golangci-lint-action` from 9.0.0 to 9.1.0 - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/0a35821d5c230e903fcfe077583637dea1b27b47...e7fa5ac41e1cf5b7d48e45e42232ce7ada589601) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-github-actions - dependency-name: actions/setup-go dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-github-actions - dependency-name: golangci/golangci-lint-action dependency-version: 9.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 6 +++--- .github/workflows/ossf-scorecard.yaml | 2 +- .github/workflows/pr-dependabot.yaml | 4 ++-- .github/workflows/release.yaml | 4 ++-- .github/workflows/verify.yml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 07c8d94383..6ebec98f4d 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -23,16 +23,16 @@ jobs: - "" - tools/setup-envtest steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 - name: Calculate go version id: vars run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # tag=v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # tag=v6.1.0 with: go-version: ${{ steps.vars.outputs.go_version }} - name: golangci-lint - uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # tag=v9.0.0 + uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # tag=v9.1.0 with: version: v2.6.1 args: --output.text.print-linter-name=true --output.text.colors=true --timeout 10m diff --git a/.github/workflows/ossf-scorecard.yaml b/.github/workflows/ossf-scorecard.yaml index 29fe19990a..a157ce2968 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 with: persist-credentials: false diff --git a/.github/workflows/pr-dependabot.yaml b/.github/workflows/pr-dependabot.yaml index 3196654902..cb52c335d3 100644 --- a/.github/workflows/pr-dependabot.yaml +++ b/.github/workflows/pr-dependabot.yaml @@ -19,12 +19,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 - name: Calculate go version id: vars run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # tag=v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # tag=v6.1.0 with: go-version: ${{ steps.vars.outputs.go_version }} - name: Update all modules diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index cf4755df09..d58a16a3d2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,12 +17,12 @@ jobs: - name: Set env run: echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV - name: Check out code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 - name: Calculate go version id: vars run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # tag=v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # tag=v6.1.0 with: go-version: ${{ steps.vars.outputs.go_version }} - name: Generate release binaries diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index a0871ec070..751f7b6b8a 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # tag=v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 - name: Check if PR title is valid env: From aa74a4063c52982b7dc8e2fbbdd305e55daf5fe5 Mon Sep 17 00:00:00 2001 From: zach593 Date: Wed, 26 Nov 2025 22:50:56 +0800 Subject: [PATCH 55/68] =?UTF-8?q?=E2=9C=A8=20Fix=20unused=20priorityqueue?= =?UTF-8?q?=20test:=20TestWhenAddingMultipleItemsWithRatelimitTrueTheyDont?= =?UTF-8?q?AffectEachOther?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: zach593 --- pkg/controller/priorityqueue/priorityqueue_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 15e0804d8d..8260126e12 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -889,7 +889,7 @@ func TestMetricsAreUpdatedForItemWhoseRequeueAfterExpiredThatGetsAddedAgainWitho }) } -func TesWhenAddingMultipleItemsWithRatelimitTrueTheyDontAffectEachOther(t *testing.T) { +func TestWhenAddingMultipleItemsWithRatelimitTrueTheyDontAffectEachOther(t *testing.T) { t.Parallel() synctest.Test(t, func(t *testing.T) { g := NewWithT(t) @@ -927,7 +927,7 @@ func TesWhenAddingMultipleItemsWithRatelimitTrueTheyDontAffectEachOther(t *testi forwardQueueTimeBy(5 * time.Millisecond) synctest.Wait() - g.Expect(retrievedItem).NotTo(BeClosed()) + g.Expect(retrievedItem).To(BeClosed()) g.Expect(retrievedSecondItem).NotTo(BeClosed()) forwardQueueTimeBy(635 * time.Millisecond) From 607e772f5d953da94e2e9e259e441717f93e0668 Mon Sep 17 00:00:00 2001 From: Godwin Pang <32888985+godwinpang@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:01:31 -0800 Subject: [PATCH 56/68] =?UTF-8?q?=F0=9F=93=96=20Add=20a=20design=20for=20s?= =?UTF-8?q?upporting=20warm=20replicas.=20(#3121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📖 Add a design for supporting warm replicas. * Address feedback. * address PR comments --- designs/warmreplicas.md | 78 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 designs/warmreplicas.md diff --git a/designs/warmreplicas.md b/designs/warmreplicas.md new file mode 100644 index 0000000000..db14247916 --- /dev/null +++ b/designs/warmreplicas.md @@ -0,0 +1,78 @@ +Add Support for Warm Replicas +============================= + +## Summary + +When controllers manage huge caches, failover takes minutes because follower replicas wait to win leader election before starting informers. “Warm replicas” allow controller-runtime to start sources while a manager instance is still on standby, so the new leader can immediately schedule workers with already-populated queues. This design documents the feature implemented in [PR #3192](https://github.com/kubernetes-sigs/controller-runtime/pull/3192) and answers the outstanding review questions. + +## Motivation + +Controllers reconcile every object from their sources at startup and after leader failover. For sources with millions of objects (e.g., Secrets, ConfigMaps, custom resources across all namespaces) the initial List+Watch can take tens of minutes, delaying recovery. Today a controller only starts its sources inside `Start`, which manager runs **after** acquiring the leader lock. That guarantees downtime equal to the cache warmup time whenever the leader rotates. + +## Goals + +- Allow controller authors to opt a controller (or all controllers) into warmup behavior with a single option (`EnableWarmup`). +- Ensure warmup never changes behavior when disabled. +- Keep the API surface minimal (no exported warmup interface yet). + +## Implemented Changes + +### Manager Warmup Phase + +Manager already buckets runnables (HTTP servers, caches, others, leader election). We added an internal `warmupRunnable` interface: + +```go +type warmupRunnable interface { + Warmup(context.Context) error +} +``` + +During `Start`, the manager now runs: + +1. HTTP servers +2. Webhooks +3. Caches +4. `Others` +5. **Warmup runnables (new)** +6. Leader election runnables once the lock is acquired + +Warmup runnables are also stopped in parallel with non-leader runnables during shutdown to avoid deadlocks. + +### Controller Opt-in + +Controllers expose the option via: + +- `ctrl.Options{Controller: config.Controller{EnableWarmup: ptr.To(true)}}` + +If both `EnableWarmup` and `NeedLeaderElection` are true, controller-runtime registers the controller as a warmup runnable. Calling `Warmup` launches the same event sources and cache sync logic as `Start`, but it does **not** start worker goroutines. Once the manager becomes leader, the controller’s normal `Start` simply spins up workers against the already-initialized queue. Enabling warmup on a controller that does not use leader election is a no-op, as the worker threads do not block on leader election being won. + +### Usage Example + +```go +mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Controller: config.Controller{ + EnableWarmup: ptr.To(true), // make every controller warm up + }, +}) +if err != nil { + panic(err) +} + +builder.ControllerManagedBy(mgr). + Named("slow-source"). + WithOptions(controller.Options{ + EnableWarmup: ptr.To(true), // optional per-controller override + }). + For(&examplev1.Example{}). + Complete(reconciler) +``` + +### Operational Considerations + +- **API server load** – Warm replicas temporarily duplicate List/Watch traffic: each standby replica performs the initial List and opens watches even though the current leader is already doing so. The additional load exists only while a replica is warming its caches, but on huge clusters this can still be expensive depending on the number of warm replicas. +- **Queue depth metrics** – Because warm replicas start their sources before workers run, the `workqueue_depth` metric spikes during warmup even though reconcilers have not begun processing. Alerting or SLOs based on that metric should either ignore the warmup window or switch to per-controller gauges that reset when workers start. + +### References + +- Implementation: [#3192](https://github.com/kubernetes-sigs/controller-runtime/pull/3192) +- Earlier context: [#2005](https://github.com/kubernetes-sigs/controller-runtime/pull/2005), [#2600](https://github.com/kubernetes-sigs/controller-runtime/issues/2600) From baa698cb3ed6fcba2401535da559755d65cc0c73 Mon Sep 17 00:00:00 2001 From: dongjiang1989 Date: Mon, 15 Dec 2025 21:09:32 +0800 Subject: [PATCH 57/68] update golangci-lint version Signed-off-by: dongjiang1989 --- .github/workflows/golangci-lint.yml | 2 +- pkg/builder/webhook_test.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 6ebec98f4d..fa771596b8 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -34,6 +34,6 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # tag=v9.1.0 with: - version: v2.6.1 + version: v2.7.2 args: --output.text.print-linter-name=true --output.text.colors=true --timeout 10m working-directory: ${{matrix.working-directory}} diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go index 55c3e11817..0bb826ea24 100644 --- a/pkg/builder/webhook_test.go +++ b/pkg/builder/webhook_test.go @@ -1153,7 +1153,7 @@ func (*TestDefaultValidatorList) DeepCopyObject() runtime.Object { return nil type TestCustomDefaulter struct{} func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { - d := obj.(*TestDefaulterObject) //nolint:ifshort + d := obj.(*TestDefaulterObject) return (&testDefaulter{}).Default(ctx, d) } @@ -1186,7 +1186,7 @@ var _ admission.CustomDefaulter = &TestCustomDefaulter{} type TestCustomValidator struct{} func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - v := obj.(*TestValidatorObject) //nolint:ifshort + v := obj.(*TestValidatorObject) return (&testValidator{}).ValidateCreate(ctx, v) } @@ -1197,7 +1197,7 @@ func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj r } func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { - v := obj.(*TestValidatorObject) //nolint:ifshort + v := obj.(*TestValidatorObject) return (&testValidator{}).ValidateDelete(ctx, v) } @@ -1280,7 +1280,7 @@ func (*TestCustomDefaultValidator) Default(ctx context.Context, obj runtime.Obje return fmt.Errorf("expected Kind TestDefaultValidator got %q", req.Kind.Kind) } - d := obj.(*TestDefaultValidator) //nolint:ifshort + d := obj.(*TestDefaultValidator) if d.Replica < 2 { d.Replica = 2 @@ -1303,7 +1303,7 @@ func (*TestCustomDefaultValidator) ValidateCreate(ctx context.Context, obj runti return nil, fmt.Errorf("expected Kind TestDefaultValidator got %q", req.Kind.Kind) } - v := obj.(*TestDefaultValidator) //nolint:ifshort + v := obj.(*TestDefaultValidator) if v.Replica < 0 { return nil, errors.New("number of replica should be greater than or equal to 0") } @@ -1341,7 +1341,7 @@ func (*TestCustomDefaultValidator) ValidateDelete(ctx context.Context, obj runti return nil, fmt.Errorf("expected Kind TestDefaultValidator got %q", req.Kind.Kind) } - v := obj.(*TestDefaultValidator) //nolint:ifshort + v := obj.(*TestDefaultValidator) if v.Replica > 0 { return nil, errors.New("number of replica should be less than or equal to 0 to delete") } From 09d0c01b2c3e25e837651f90ea1d39bf56bb1c4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:23:12 +0000 Subject: [PATCH 58/68] :seedling: Bump the all-github-actions group across 1 directory with 4 updates Bumps the all-github-actions group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `actions/checkout` from 6.0.0 to 6.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/1af3b93b6815bc44a9784bd300feb67ff0d1eeb3...8e8c483db84b4bee98b60c0593521ed34d9990e8) Updates `golangci/golangci-lint-action` from 9.1.0 to 9.2.0 - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/e7fa5ac41e1cf5b7d48e45e42232ce7ada589601...1e7e51e771db61008b38414a730f564565cf7c20) Updates `actions/upload-artifact` from 5.0.0 to 6.0.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/330a01c490aca151604b8cf639adc76d48f6c5d4...b7c566a772e6b6bfb58ed0dc250532a479d7789f) Updates `softprops/action-gh-release` from 2.4.2 to 2.5.0 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/5be0e66d93ac7ed76da52eca8bb058f665c3a5fe...a06a81a03ee405af7f2048a818ed3f03bbf83c7b) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-github-actions - dependency-name: golangci/golangci-lint-action dependency-version: 9.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-github-actions - dependency-name: actions/upload-artifact dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-github-actions - dependency-name: softprops/action-gh-release dependency-version: 2.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 4 ++-- .github/workflows/ossf-scorecard.yaml | 4 ++-- .github/workflows/pr-dependabot.yaml | 2 +- .github/workflows/release.yaml | 4 ++-- .github/workflows/verify.yml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index fa771596b8..7c44a68de9 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # tag=v6.0.1 - name: Calculate go version id: vars run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT @@ -32,7 +32,7 @@ jobs: with: go-version: ${{ steps.vars.outputs.go_version }} - name: golangci-lint - uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # tag=v9.1.0 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # tag=v9.2.0 with: version: v2.7.2 args: --output.text.print-linter-name=true --output.text.colors=true --timeout 10m diff --git a/.github/workflows/ossf-scorecard.yaml b/.github/workflows/ossf-scorecard.yaml index a157ce2968..d45d674957 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # tag=v6.0.1 with: persist-credentials: false @@ -43,7 +43,7 @@ jobs: # Upload the results as artifacts. - name: "Upload artifact" - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # tag=v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # tag=v6.0.0 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/pr-dependabot.yaml b/.github/workflows/pr-dependabot.yaml index cb52c335d3..344b6ac4c7 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # tag=v6.0.1 - 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 d58a16a3d2..edc14d0b95 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # tag=v6.0.1 - name: Calculate go version id: vars run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT @@ -29,7 +29,7 @@ jobs: run: | make release - name: Release - uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # tag=v2.4.2 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # tag=v2.5.0 with: draft: false files: tools/setup-envtest/out/* diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 751f7b6b8a..451f63c01c 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # tag=v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # tag=v6.0.1 - name: Check if PR title is valid env: From 56d9e70af999e22a74482b636dcd648a31b405df Mon Sep 17 00:00:00 2001 From: Kevin Hannon Date: Wed, 17 Dec 2025 22:26:27 -0500 Subject: [PATCH 59/68] update to k8s 0.35 --- examples/scratch-env/go.mod | 18 ++++++------- examples/scratch-env/go.sum | 44 +++++++++++++++---------------- go.mod | 26 +++++++++---------- go.sum | 52 ++++++++++++++++++------------------- tools/setup-envtest/go.mod | 14 +++++----- tools/setup-envtest/go.sum | 28 ++++++++++---------- 6 files changed, 91 insertions(+), 91 deletions(-) diff --git a/examples/scratch-env/go.mod b/examples/scratch-env/go.mod index 5051a1de4e..06f58bb989 100644 --- a/examples/scratch-env/go.mod +++ b/examples/scratch-env/go.mod @@ -40,22 +40,22 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.0-beta.0 // indirect - k8s.io/apiextensions-apiserver v0.35.0-beta.0 // indirect - k8s.io/apimachinery v0.35.0-beta.0 // indirect - k8s.io/client-go v0.35.0-beta.0 // indirect + k8s.io/api v0.35.0 // indirect + k8s.io/apiextensions-apiserver v0.35.0 // indirect + k8s.io/apimachinery v0.35.0 // indirect + k8s.io/client-go v0.35.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect diff --git a/examples/scratch-env/go.sum b/examples/scratch-env/go.sum index 4a8322fbb3..ef2cb38f06 100644 --- a/examples/scratch-env/go.sum +++ b/examples/scratch-env/go.sum @@ -114,24 +114,24 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= @@ -146,14 +146,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.35.0-beta.0 h1:eqAAVeSatXNnsPjaeFrFGqSl5ihtPY4e8Txy2nYPOnw= -k8s.io/api v0.35.0-beta.0/go.mod h1:UXuvkssy8lHPSP381eqqBOW4BvRTicVpRjv7k2sjo4Y= -k8s.io/apiextensions-apiserver v0.35.0-beta.0 h1:1e0ar0DsUPqR0G6RPHXGVe7G/+Grex+pUF8hXu5+OxE= -k8s.io/apiextensions-apiserver v0.35.0-beta.0/go.mod h1:/UUhEsqEZ5q4TZzGFvAf4V/x00lyryOiLJsL5oD9BGM= -k8s.io/apimachinery v0.35.0-beta.0 h1:vVoDiASLwUEv5yZceZCBRPXBc1f9wUOZs7ZbEbGr5sY= -k8s.io/apimachinery v0.35.0-beta.0/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= -k8s.io/client-go v0.35.0-beta.0 h1:4APvMU7+XwWF+XoqAv+gbtSmwjPCXXXo4XVcY89Rde0= -k8s.io/client-go v0.35.0-beta.0/go.mod h1:+XxnPEoaCIB5G0zpwXRh3AnT+CvgS5lA+AFr9EtHUcA= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/go.mod b/go.mod index e545f60984..b06164f275 100644 --- a/go.mod +++ b/go.mod @@ -16,16 +16,16 @@ require ( github.com/prometheus/client_model v0.6.2 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 - golang.org/x/mod v0.28.0 - golang.org/x/sync v0.17.0 - golang.org/x/sys v0.37.0 + golang.org/x/mod v0.29.0 + golang.org/x/sync v0.18.0 + golang.org/x/sys v0.38.0 gomodules.xyz/jsonpatch/v2 v2.4.0 gopkg.in/evanphx/json-patch.v4 v4.13.0 // Using v4 to match upstream - k8s.io/api v0.35.0-beta.0 - k8s.io/apiextensions-apiserver v0.35.0-beta.0 - k8s.io/apimachinery v0.35.0-beta.0 - k8s.io/apiserver v0.35.0-beta.0 - k8s.io/client-go v0.35.0-beta.0 + k8s.io/api v0.35.0 + k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery v0.35.0 + k8s.io/apiserver v0.35.0 + k8s.io/client-go v0.35.0 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/structured-merge-diff/v6 v6.3.0 @@ -82,19 +82,19 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/component-base v0.35.0-beta.0 // indirect + k8s.io/component-base v0.35.0 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index 27b1b405ea..938f89efc2 100644 --- a/go.sum +++ b/go.sum @@ -185,24 +185,24 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= @@ -223,18 +223,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.35.0-beta.0 h1:eqAAVeSatXNnsPjaeFrFGqSl5ihtPY4e8Txy2nYPOnw= -k8s.io/api v0.35.0-beta.0/go.mod h1:UXuvkssy8lHPSP381eqqBOW4BvRTicVpRjv7k2sjo4Y= -k8s.io/apiextensions-apiserver v0.35.0-beta.0 h1:1e0ar0DsUPqR0G6RPHXGVe7G/+Grex+pUF8hXu5+OxE= -k8s.io/apiextensions-apiserver v0.35.0-beta.0/go.mod h1:/UUhEsqEZ5q4TZzGFvAf4V/x00lyryOiLJsL5oD9BGM= -k8s.io/apimachinery v0.35.0-beta.0 h1:vVoDiASLwUEv5yZceZCBRPXBc1f9wUOZs7ZbEbGr5sY= -k8s.io/apimachinery v0.35.0-beta.0/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= -k8s.io/apiserver v0.35.0-beta.0 h1:V0daLhmZy3hqohfz1pDImWH+Js4mI02E5Sv2Zin96f4= -k8s.io/apiserver v0.35.0-beta.0/go.mod h1:XvMSZG0iw7xUDsaQIHf36L0TomhYAbgCCc4TeLgkovU= -k8s.io/client-go v0.35.0-beta.0 h1:4APvMU7+XwWF+XoqAv+gbtSmwjPCXXXo4XVcY89Rde0= -k8s.io/client-go v0.35.0-beta.0/go.mod h1:+XxnPEoaCIB5G0zpwXRh3AnT+CvgS5lA+AFr9EtHUcA= -k8s.io/component-base v0.35.0-beta.0 h1:zqaQLtIs5VDBNsg4A/1Nkq2pC7fQhcgcvwHRgI7utFE= -k8s.io/component-base v0.35.0-beta.0/go.mod h1:gkWiSIt+PGyxlzWzy/8PGqKvKsLK6mujscMs+Qjzgn4= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= +k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/tools/setup-envtest/go.mod b/tools/setup-envtest/go.mod index dcccfe130f..053ce8b99c 100644 --- a/tools/setup-envtest/go.mod +++ b/tools/setup-envtest/go.mod @@ -10,7 +10,7 @@ require ( github.com/spf13/afero v1.12.0 github.com/spf13/pflag v1.0.9 go.uber.org/zap v1.27.0 - k8s.io/apimachinery v0.35.0-beta.0 + k8s.io/apimachinery v0.35.0 sigs.k8s.io/yaml v1.6.0 ) @@ -22,11 +22,11 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/tools/setup-envtest/go.sum b/tools/setup-envtest/go.sum index 1769902891..bbcf8021f5 100644 --- a/tools/setup-envtest/go.sum +++ b/tools/setup-envtest/go.sum @@ -62,18 +62,18 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -81,7 +81,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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.35.0-beta.0 h1:vVoDiASLwUEv5yZceZCBRPXBc1f9wUOZs7ZbEbGr5sY= -k8s.io/apimachinery v0.35.0-beta.0/go.mod h1:dR9KPaf5L0t2p9jZg/wCGB4b3ma2sXZ2zdNqILs+Sak= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 7fb375a4991a8745478768b600a6687da8203002 Mon Sep 17 00:00:00 2001 From: Stefan Bueringer Date: Thu, 18 Dec 2025 16:24:12 +0100 Subject: [PATCH 60/68] Fix fake client Apply with Unstructured ApplyConfiguration + resourceVersion unset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Büringer buringerst@vmware.com --- pkg/client/client_test.go | 40 ++++++++++++++++++++++++++++ pkg/client/fake/client_test.go | 19 +++++++++++++ pkg/client/fake/versioned_tracker.go | 7 +++-- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index b2890c385d..878927f467 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -955,6 +955,7 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actualData).To(BeComparableTo(data)) Expect(actualData).To(BeComparableTo(obj.Object["data"])) + // Apply with ResourceVersion set data = map[string]any{ "a-new-key": "a-new-value", } @@ -974,6 +975,28 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actualData).To(BeComparableTo(data)) Expect(actualData).To(BeComparableTo(obj.Object["data"])) + + // Apply with ResourceVersion unset + obj.SetResourceVersion("") + data = map[string]any{ + "another-new-key": "another-new-value", + } + obj.Object["data"] = data + unstructured.RemoveNestedField(obj.Object, "metadata", "managedFields") + + err = cl.Apply(ctx, client.ApplyConfigurationFromUnstructured(obj), &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + cm, err = clientset.CoreV1().ConfigMaps(obj.GetNamespace()).Get(ctx, obj.GetName(), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + actualData = map[string]any{} + for k, v := range cm.Data { + actualData[k] = v + } + + Expect(actualData).To(BeComparableTo(data)) + Expect(actualData).To(BeComparableTo(obj.Object["data"])) }) }) @@ -999,6 +1022,23 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(cm.Data).To(BeComparableTo(data)) Expect(cm.Data).To(BeComparableTo(obj.Data)) + // Apply with ResourceVersion set + data = map[string]string{ + "a-new-key": "a-new-value", + } + obj.Data = data + + err = cl.Apply(ctx, obj, &client.ApplyOptions{FieldManager: "test-manager"}) + Expect(err).NotTo(HaveOccurred()) + + cm, err = clientset.CoreV1().ConfigMaps(ptr.Deref(obj.GetNamespace(), "")).Get(ctx, ptr.Deref(obj.GetName(), ""), metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(cm.Data).To(BeComparableTo(data)) + Expect(cm.Data).To(BeComparableTo(obj.Data)) + + // Apply with ResourceVersion unset + obj.ResourceVersion = ptr.To("") data = map[string]string{ "a-new-key": "a-new-value", } diff --git a/pkg/client/fake/client_test.go b/pkg/client/fake/client_test.go index 1901b3051d..209ccc67fe 100644 --- a/pkg/client/fake/client_test.go +++ b/pkg/client/fake/client_test.go @@ -2875,11 +2875,20 @@ var _ = Describe("Fake client", func() { Expect(cl.Get(ctx, client.ObjectKeyFromObject(cm), cm)).To(Succeed()) Expect(cm.Data).To(BeComparableTo(map[string]string{"some": "data"})) + // Apply with ResourceVersion set 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"})) + + // Apply with ResourceVersion unset + obj.ResourceVersion = ptr.To("") + obj.Data = map[string]string{"another": "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{"another": "data"})) }) It("returns a conflict when trying to Create an object with UID set through Apply", func(ctx SpecContext) { @@ -2931,12 +2940,22 @@ var _ = Describe("Fake client", func() { Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) Expect(result.Object["spec"]).To(Equal(map[string]any{"some": "data"})) + // Apply with ResourceVersion set Expect(unstructured.SetNestedField(obj.Object, map[string]any{"other": "data"}, "spec")).To(Succeed()) applyConfig2 := client.ApplyConfigurationFromUnstructured(obj) Expect(cl.Apply(ctx, applyConfig2, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) Expect(result.Object["spec"]).To(Equal(map[string]any{"other": "data"})) + + // Apply with ResourceVersion unset + obj.SetResourceVersion("") + Expect(unstructured.SetNestedField(obj.Object, map[string]any{"another": "data"}, "spec")).To(Succeed()) + applyConfig3 := client.ApplyConfigurationFromUnstructured(obj) + Expect(cl.Apply(ctx, applyConfig3, &client.ApplyOptions{FieldManager: "test-manager"})).To(Succeed()) + + Expect(cl.Get(ctx, client.ObjectKeyFromObject(result), result)).To(Succeed()) + Expect(result.Object["spec"]).To(Equal(map[string]any{"another": "data"})) }) It("supports server-side apply of a custom resource via Apply method after List with a non-list kind", func(ctx SpecContext) { diff --git a/pkg/client/fake/versioned_tracker.go b/pkg/client/fake/versioned_tracker.go index bc1eaeb951..bbe3ac9b0d 100644 --- a/pkg/client/fake/versioned_tracker.go +++ b/pkg/client/fake/versioned_tracker.go @@ -265,11 +265,14 @@ func (t versionedTracker) updateObject( // apiserver accepts such a patch, but it does so we just copy that behavior. // Kubernetes apiserver behavior can be checked like this: // `kubectl patch configmap foo --patch '{"metadata":{"annotations":{"foo":"bar"},"resourceVersion":null}}' -v=9` - case bytes. - Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")): + case bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Patch")): // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change // that reaction, we use the callstack to figure out if this originated from the "fakeClient.Patch" func. accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) + case bytes.Contains(debug.Stack(), []byte("sigs.k8s.io/controller-runtime/pkg/client/fake.(*fakeClient).Apply")): + // We apply patches using a client-go reaction that ends up calling the trackers Update. As we can't change + // that reaction, we use the callstack to figure out if this originated from the "fakeClient.Apply" func. + accessor.SetResourceVersion(oldAccessor.GetResourceVersion()) } } From 65334dbe6aa10b5cb78e69a3bdc62bbd908cc5ba Mon Sep 17 00:00:00 2001 From: dongjiang1989 Date: Mon, 22 Dec 2025 20:49:37 +0800 Subject: [PATCH 61/68] update controller-tools to 0.20.0 and fix lint Signed-off-by: dongjiang1989 --- .golangci.yml | 1 + Makefile | 2 +- pkg/internal/flock/flock_other.go | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 9c11b8e816..5c86af65a3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -60,6 +60,7 @@ linters: disable: - fieldalignment - shadow + - buildtag enable-all: true importas: alias: diff --git a/Makefile b/Makefile index 86a5eddf80..1c1fb7f429 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,7 @@ GO_APIDIFF_PKG := github.com/joelanford/go-apidiff $(GO_APIDIFF): # Build go-apidiff from tools folder. GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GO_APIDIFF_PKG) $(GO_APIDIFF_BIN) $(GO_APIDIFF_VER) -CONTROLLER_GEN_VER := v0.17.1 +CONTROLLER_GEN_VER := v0.20.0 CONTROLLER_GEN_BIN := controller-gen CONTROLLER_GEN := $(abspath $(TOOLS_BIN_DIR)/$(CONTROLLER_GEN_BIN)-$(CONTROLLER_GEN_VER)) CONTROLLER_GEN_PKG := sigs.k8s.io/controller-tools/cmd/controller-gen diff --git a/pkg/internal/flock/flock_other.go b/pkg/internal/flock/flock_other.go index 1def472197..33c7a0799c 100644 --- a/pkg/internal/flock/flock_other.go +++ b/pkg/internal/flock/flock_other.go @@ -1,4 +1,5 @@ //go:build !linux && !darwin && !freebsd && !openbsd && !netbsd && !dragonfly +// +build !linux,!darwin,!freebsd,!openbsd,!netbsd,!dragonfly /* Copyright 2016 The Kubernetes Authors. From ffdec086505b97ec40d2bc36ffa75d73bf0f1972 Mon Sep 17 00:00:00 2001 From: dongjiang1989 Date: Mon, 22 Dec 2025 20:56:23 +0800 Subject: [PATCH 62/68] fix Signed-off-by: dongjiang1989 --- pkg/internal/flock/flock_other.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/internal/flock/flock_other.go b/pkg/internal/flock/flock_other.go index 33c7a0799c..1def472197 100644 --- a/pkg/internal/flock/flock_other.go +++ b/pkg/internal/flock/flock_other.go @@ -1,5 +1,4 @@ //go:build !linux && !darwin && !freebsd && !openbsd && !netbsd && !dragonfly -// +build !linux,!darwin,!freebsd,!openbsd,!netbsd,!dragonfly /* Copyright 2016 The Kubernetes Authors. From 23ce86403cc9345813d08432982acd5673ab115a Mon Sep 17 00:00:00 2001 From: Rafael Brito Date: Wed, 7 Jan 2026 09:18:14 -0600 Subject: [PATCH 63/68] adding exponential buckets on webhook native histogram Signed-off-by: Rafael Brito --- pkg/webhook/internal/metrics/metrics.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/webhook/internal/metrics/metrics.go b/pkg/webhook/internal/metrics/metrics.go index f1e6ce68f5..a8ff400954 100644 --- a/pkg/webhook/internal/metrics/metrics.go +++ b/pkg/webhook/internal/metrics/metrics.go @@ -33,6 +33,7 @@ var ( prometheus.HistogramOpts{ Name: "controller_runtime_webhook_latency_seconds", Help: "Histogram of the latency of processing admission requests", + Buckets: prometheus.ExponentialBuckets(10e-9, 10, 12), NativeHistogramBucketFactor: 1.1, NativeHistogramMaxBucketNumber: 100, NativeHistogramMinResetDuration: 1 * time.Hour, From 31d30b6696c75a8b6fc29a0b85b7686eefecca72 Mon Sep 17 00:00:00 2001 From: zach593 Date: Fri, 2 Jan 2026 11:02:03 +0800 Subject: [PATCH 64/68] fix priority queue ordering when item priority changes Signed-off-by: zach593 --- pkg/controller/priorityqueue/priorityqueue.go | 11 +++++++++- .../priorityqueue/priorityqueue_test.go | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/controller/priorityqueue/priorityqueue.go b/pkg/controller/priorityqueue/priorityqueue.go index 71363f0d17..c0e291c2a1 100644 --- a/pkg/controller/priorityqueue/priorityqueue.go +++ b/pkg/controller/priorityqueue/priorityqueue.go @@ -30,6 +30,13 @@ type AddOpts struct { // internally de-duplicates all items that are added to // it. It will use the max of the passed priorities and the // min of possible durations. +// +// When an item that is already enqueued at a lower priority +// is re-enqueued with a higher priority, it will be placed at +// the end among items of the new priority, in order to +// preserve FIFO semantics within each priority level. +// The effective duration (i.e. the ready time) is still +// computed as the minimum across all enqueues. type PriorityQueue[T comparable] interface { workqueue.TypedRateLimitingInterface[T] AddWithOpts(o AddOpts, Items ...T) @@ -161,12 +168,12 @@ func (w *priorityqueue[T]) AddWithOpts(o AddOpts, items ...T) { Priority: ptr.Deref(o.Priority, 0), ReadyAt: readyAt, } + w.addedCounter++ w.items[key] = item w.queue.ReplaceOrInsert(item) if item.ReadyAt == nil { w.metrics.add(key, item.Priority) } - w.addedCounter++ continue } @@ -179,6 +186,8 @@ func (w *priorityqueue[T]) AddWithOpts(o AddOpts, items ...T) { w.metrics.updateDepthWithPriorityMetric(item.Priority, newPriority) } item.Priority = newPriority + item.AddedCounter = w.addedCounter + w.addedCounter++ } if item.ReadyAt != nil && (readyAt == nil || readyAt.Before(*item.ReadyAt)) { diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 8260126e12..989febdbcf 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -320,6 +320,26 @@ var _ = Describe("Controllerworkqueue", func() { Expect(depth).To(Equal(0)) } }) + + It("follows FIFO order in the new priority queue when item priority changes", func() { + q, _ := newQueue() + defer q.ShutDown() + + q.AddWithOpts(AddOpts{Priority: ptr.To(0)}, "foo") + q.AddWithOpts(AddOpts{Priority: ptr.To(1)}, "bar") + q.AddWithOpts(AddOpts{Priority: ptr.To(1)}, "foo") + Expect(q.Len()).To(Equal(2)) + + item, priority, _ := q.GetWithPriority() + Expect(item).To(Equal("bar")) + Expect(priority).To(Equal(1)) + Expect(q.Len()).To(Equal(1)) + + item, priority, _ = q.GetWithPriority() + Expect(item).To(Equal("foo")) + Expect(priority).To(Equal(1)) + Expect(q.Len()).To(Equal(0)) + }) }) func BenchmarkAddGetDone(b *testing.B) { From 9de69a734f9125a15d06a001320cc758aaec87e1 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sat, 1 Nov 2025 19:39:46 -0400 Subject: [PATCH 65/68] :seedling: Priorityqueue: Use separate b-trees for ready and non-ready items This change refactors the priorityqueue to use distinct b-trees for ready and non-ready items. This simplifies the code, because the two of them effectively have different sorting requirements. We made this work using one b-tree sorted by priority first and readyAt second and then iterating through all priorities high to low until we find a ready item. While correct, this is difficult to reason about. The other issue is that currently, there is no explicit transition step from non-ready to ready. As a result, we always had to iterate over all items in case one got ready to then update metrics. With this change, we instead have an explicit event when an item gets ready and we can then update metrics for just this item and do not have to iterate over everything. --- pkg/controller/priorityqueue/priorityqueue.go | 304 ++++++++++-------- .../priorityqueue/priorityqueue_test.go | 44 ++- 2 files changed, 208 insertions(+), 140 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue.go b/pkg/controller/priorityqueue/priorityqueue.go index c0e291c2a1..3fbc127050 100644 --- a/pkg/controller/priorityqueue/priorityqueue.go +++ b/pkg/controller/priorityqueue/priorityqueue.go @@ -1,7 +1,6 @@ package priorityqueue import ( - "math" "sync" "sync/atomic" "time" @@ -71,25 +70,27 @@ func New[T comparable](name string, o ...Opt[T]) PriorityQueue[T] { } pq := &priorityqueue[T]{ - log: opts.Log, - items: map[T]*item[T]{}, - queue: btree.NewG(32, less[T]), - becameReady: sets.Set[T]{}, - metrics: newQueueMetrics[T](opts.MetricProvider, name, clock.RealClock{}), - // itemOrWaiterAdded indicates that an item or + log: opts.Log, + items: map[T]*item[T]{}, + ready: btree.NewG(32, lessReady[T]), + waiting: btree.NewG(32, lessWaiting[T]), + metrics: newQueueMetrics[T](opts.MetricProvider, name, clock.RealClock{}), + // readyItemOrWaiterAdded indicates that a ready item or // waiter was added. It must be buffered, because // if we currently process items we can't tell // if that included the new item/waiter. - itemOrWaiterAdded: make(chan struct{}, 1), - rateLimiter: opts.RateLimiter, - locked: sets.Set[T]{}, - done: make(chan struct{}), - get: make(chan item[T]), - now: time.Now, - tick: time.Tick, + readyItemOrWaiterAdded: make(chan struct{}, 1), + waitingItemAddedOrUpdated: make(chan struct{}, 1), + rateLimiter: opts.RateLimiter, + locked: sets.Set[T]{}, + done: make(chan struct{}), + get: make(chan item[T]), + now: time.Now, + tick: time.Tick, } - go pq.spin() + go pq.handleReadyItems() + go pq.handleWaitingItems() go pq.logState() if _, ok := pq.metrics.(noMetrics[T]); !ok { go pq.updateUnfinishedWorkLoop() @@ -100,30 +101,28 @@ func New[T comparable](name string, o ...Opt[T]) PriorityQueue[T] { type priorityqueue[T comparable] struct { log logr.Logger - // lock has to be acquired for any access any of items, queue, addedCounter - // or becameReady - lock sync.Mutex - items map[T]*item[T] - queue bTree[*item[T]] + // lock has to be acquired for any access to any of items, ready, waiting, + // addedCounter or waiters. + lock sync.Mutex + items map[T]*item[T] + ready bTree[*item[T]] + waiting bTree[*item[T]] // addedCounter is a counter of elements added, we need it - // because unixNano is not guaranteed to be unique. + // to provide FIFO semantics. addedCounter uint64 - // becameReady holds items that are in the queue, were added - // with non-zero after and became ready. We need it to call the - // metrics add exactly once for them. - becameReady sets.Set[T] - metrics queueMetrics[T] + metrics queueMetrics[T] - itemOrWaiterAdded chan struct{} + readyItemOrWaiterAdded chan struct{} + waitingItemAddedOrUpdated chan struct{} rateLimiter workqueue.TypedRateLimiter[T] // locked contains the keys we handed out through Get() and that haven't // yet been returned through Done(). locked sets.Set[T] - lockedLock sync.RWMutex + lockedLock sync.Mutex shutdown atomic.Bool done chan struct{} @@ -144,6 +143,9 @@ func (w *priorityqueue[T]) AddWithOpts(o AddOpts, items ...T) { return } + var readyItemAdded bool + var waitingItemAddedOrUpdated bool + w.lock.Lock() defer w.lock.Unlock() @@ -170,68 +172,147 @@ func (w *priorityqueue[T]) AddWithOpts(o AddOpts, items ...T) { } w.addedCounter++ w.items[key] = item - w.queue.ReplaceOrInsert(item) - if item.ReadyAt == nil { + if readyAt != nil { + w.waiting.ReplaceOrInsert(item) + waitingItemAddedOrUpdated = true + } else { + w.ready.ReplaceOrInsert(item) w.metrics.add(key, item.Priority) + readyItemAdded = true } continue } - // The b-tree de-duplicates based on ordering and any change here - // will affect the order - Just delete and re-add. - item, _ := w.queue.Delete(w.items[key]) - if newPriority := ptr.Deref(o.Priority, 0); newPriority > item.Priority { - // Update depth metric only if the item in the queue was already added to the depth metric. - if item.ReadyAt == nil || w.becameReady.Has(key) { - w.metrics.updateDepthWithPriorityMetric(item.Priority, newPriority) + if w.items[key].ReadyAt == nil { + readyAt = nil + } else if readyAt != nil && w.items[key].ReadyAt.Before(*readyAt) { + readyAt = w.items[key].ReadyAt + } + + priority := w.items[key].Priority + addedCounter := w.items[key].AddedCounter + if newPriority := ptr.Deref(o.Priority, 0); newPriority > w.items[key].Priority { + // Update depth metric only if the item was already ready + if w.items[key].ReadyAt == nil { + w.metrics.updateDepthWithPriorityMetric(w.items[key].Priority, newPriority) } - item.Priority = newPriority - item.AddedCounter = w.addedCounter + priority = newPriority + addedCounter = w.addedCounter w.addedCounter++ } - if item.ReadyAt != nil && (readyAt == nil || readyAt.Before(*item.ReadyAt)) { - if readyAt == nil && !w.becameReady.Has(key) { - w.metrics.add(key, item.Priority) - } - item.ReadyAt = readyAt + var tree, previousTree bTree[*item[T]] + switch { + case readyAt == nil && w.items[key].ReadyAt == nil: + tree, previousTree = w.ready, w.ready + case readyAt == nil && w.items[key].ReadyAt != nil: + tree, previousTree = w.ready, w.waiting + readyItemAdded = true + w.metrics.add(key, priority) + case readyAt != nil: + // We are in the update path and we set readyAt to nil if the + // existing item has a nil readyAt, so we can be sure here that + // it has a non-nil readyAt/is in w.waiting. + tree, previousTree = w.waiting, w.waiting + waitingItemAddedOrUpdated = true } - w.queue.ReplaceOrInsert(item) + item, _ := previousTree.Delete(w.items[key]) + item.ReadyAt = readyAt + item.Priority = priority + item.AddedCounter = addedCounter + tree.ReplaceOrInsert(item) } - if len(items) > 0 { - w.notifyItemOrWaiterAdded() + if readyItemAdded { + w.notifyReadyItemOrWaiterAdded() + } + if waitingItemAddedOrUpdated { + w.notifyWaitingItemAddedOrUpdated() } } -func (w *priorityqueue[T]) notifyItemOrWaiterAdded() { +func (w *priorityqueue[T]) notifyReadyItemOrWaiterAdded() { select { - case w.itemOrWaiterAdded <- struct{}{}: + case w.readyItemOrWaiterAdded <- struct{}{}: default: } } -func (w *priorityqueue[T]) spin() { +func (w *priorityqueue[T]) notifyWaitingItemAddedOrUpdated() { + select { + case w.waitingItemAddedOrUpdated <- struct{}{}: + default: + } +} + +func (w *priorityqueue[T]) handleWaitingItems() { blockForever := make(chan time.Time) var nextReady <-chan time.Time nextReady = blockForever - var nextItemReadyAt time.Time for { select { case <-w.done: return - case <-w.itemOrWaiterAdded: + case <-w.waitingItemAddedOrUpdated: case <-nextReady: nextReady = blockForever - nextItemReadyAt = time.Time{} } func() { w.lock.Lock() defer w.lock.Unlock() + var toMove []*item[T] + w.waiting.Ascend(func(item *item[T]) bool { + readyIn := item.ReadyAt.Sub(w.now()) // Store this to prevent TOCTOU issues + if readyIn <= 0 { + toMove = append(toMove, item) + return true + } + + nextReady = w.tick(readyIn) + return false + }) + + // Don't manipulate the tree from within Ascend + for _, toMove := range toMove { + w.waiting.Delete(toMove) + toMove.ReadyAt = nil + + // Bump added counter so items get sorted by when + // they became ready, not when they were added. + toMove.AddedCounter = w.addedCounter + w.addedCounter++ + + w.metrics.add(toMove.Key, toMove.Priority) + w.ready.ReplaceOrInsert(toMove) + } + + if len(toMove) > 0 { + w.notifyReadyItemOrWaiterAdded() + } + }() + } +} + +func (w *priorityqueue[T]) handleReadyItems() { + for { + select { + case <-w.done: + return + case <-w.readyItemOrWaiterAdded: + } + + func() { + w.lock.Lock() + defer w.lock.Unlock() + + if w.waiters == 0 { + return + } + w.lockedLock.Lock() defer w.lockedLock.Unlock() @@ -239,69 +320,24 @@ func (w *priorityqueue[T]) spin() { // track what we want to delete and do it after we are done ascending. var toDelete []*item[T] - var key T - - // Items in the queue tree are sorted first by priority and second by readiness, so - // items with a lower priority might be ready further down in the queue. - // We iterate through the priorities high to low until we find a ready item - pivot := item[T]{ - Key: key, - AddedCounter: 0, - Priority: math.MaxInt, - ReadyAt: nil, - } - - for { - pivotChange := false - - w.queue.AscendGreaterOrEqual(&pivot, func(item *item[T]) bool { - // Item is locked, we can not hand it out - if w.locked.Has(item.Key) { - return true - } - - if item.ReadyAt != nil { - if readyAt := item.ReadyAt.Sub(w.now()); readyAt > 0 { - if nextItemReadyAt.After(*item.ReadyAt) || nextItemReadyAt.IsZero() { - nextReady = w.tick(readyAt) - nextItemReadyAt = *item.ReadyAt - } - - // Adjusting the pivot item moves the ascend to the next lower priority - pivot.Priority = item.Priority - 1 - pivotChange = true - return false - } - if !w.becameReady.Has(item.Key) { - w.metrics.add(item.Key, item.Priority) - w.becameReady.Insert(item.Key) - } - } - - if w.waiters == 0 { - // Have to keep iterating here to ensure we update metrics - // for further items that became ready and set nextReady. - return true - } - - w.metrics.get(item.Key, item.Priority) - w.locked.Insert(item.Key) - w.waiters-- - delete(w.items, item.Key) - toDelete = append(toDelete, item) - w.becameReady.Delete(item.Key) - w.get <- *item - + w.ready.Ascend(func(item *item[T]) bool { + // Item is locked, we can not hand it out + if w.locked.Has(item.Key) { return true - }) - - if !pivotChange { - break } - } + + w.metrics.get(item.Key, item.Priority) + w.locked.Insert(item.Key) + w.waiters-- + delete(w.items, item.Key) + toDelete = append(toDelete, item) + w.get <- *item + + return w.waiters > 0 + }) for _, item := range toDelete { - w.queue.Delete(item) + w.ready.Delete(item) } }() } @@ -329,7 +365,7 @@ func (w *priorityqueue[T]) GetWithPriority() (_ T, priority int, shutdown bool) w.waiters++ w.lock.Unlock() - w.notifyItemOrWaiterAdded() + w.notifyReadyItemOrWaiterAdded() select { case <-w.done: @@ -367,7 +403,7 @@ func (w *priorityqueue[T]) Done(item T) { defer w.lockedLock.Unlock() w.locked.Delete(item) w.metrics.done(item) - w.notifyItemOrWaiterAdded() + w.notifyReadyItemOrWaiterAdded() } func (w *priorityqueue[T]) ShutDown() { @@ -388,16 +424,7 @@ func (w *priorityqueue[T]) Len() int { w.lock.Lock() defer w.lock.Unlock() - var result int - w.queue.Ascend(func(item *item[T]) bool { - if item.ReadyAt == nil || item.ReadyAt.Compare(w.now()) <= 0 { - result++ - return true - } - return false - }) - - return result + return w.ready.Len() } func (w *priorityqueue[T]) logState() { @@ -417,7 +444,11 @@ func (w *priorityqueue[T]) logState() { } w.lock.Lock() items := make([]*item[T], 0, len(w.items)) - w.queue.Ascend(func(item *item[T]) bool { + w.waiting.Ascend(func(item *item[T]) bool { + items = append(items, item) + return true + }) + w.ready.Ascend(func(item *item[T]) bool { items = append(items, item) return true }) @@ -427,20 +458,17 @@ func (w *priorityqueue[T]) logState() { } } -func less[T comparable](a, b *item[T]) bool { - if a.Priority != b.Priority { - return a.Priority > b.Priority - } - if a.ReadyAt == nil && b.ReadyAt != nil { - return true - } - if b.ReadyAt == nil && a.ReadyAt != nil { - return false - } - if a.ReadyAt != nil && b.ReadyAt != nil && !a.ReadyAt.Equal(*b.ReadyAt) { +func lessWaiting[T comparable](a, b *item[T]) bool { + if !a.ReadyAt.Equal(*b.ReadyAt) { return a.ReadyAt.Before(*b.ReadyAt) } + return lessReady(a, b) +} +func lessReady[T comparable](a, b *item[T]) bool { + if a.Priority != b.Priority { + return a.Priority > b.Priority + } return a.AddedCounter < b.AddedCounter } @@ -464,8 +492,8 @@ func (w *priorityqueue[T]) updateUnfinishedWorkLoop() { } type bTree[T any] interface { - ReplaceOrInsert(item T) (_ T, _ bool) + ReplaceOrInsert(item T) (T, bool) Delete(item T) (T, bool) Ascend(iterator btree.ItemIteratorG[T]) - AscendGreaterOrEqual(pivot T, iterator btree.ItemIteratorG[T]) + Len() int } diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 989febdbcf..0a1099e053 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -595,8 +595,13 @@ func newQueue() (*priorityqueue[string], *fakeMetricsProvider) { q := New("test", func(o *Opts[string]) { o.MetricProvider = metrics }) - q.(*priorityqueue[string]).queue = &btreeInteractionValidator{ - bTree: q.(*priorityqueue[string]).queue, + q.(*priorityqueue[string]).waiting = &btreeInteractionValidator{ + bTree: q.(*priorityqueue[string]).waiting, + shouldHaveReadyAt: true, + } + q.(*priorityqueue[string]).ready = &btreeInteractionValidator{ + bTree: q.(*priorityqueue[string]).ready, + shouldHaveReadyAt: false, } // validate that tick always gets a positive value as it will just return @@ -613,9 +618,13 @@ func newQueue() (*priorityqueue[string], *fakeMetricsProvider) { type btreeInteractionValidator struct { bTree[*item[string]] + shouldHaveReadyAt bool } func (b *btreeInteractionValidator) ReplaceOrInsert(item *item[string]) (*item[string], bool) { + if hasReadyAt := item.ReadyAt != nil; hasReadyAt != b.shouldHaveReadyAt { + panic(fmt.Sprintf("ReplaceOrInsert: item has unexpected ReadyAt presence: %v, expected: %v", hasReadyAt, b.shouldHaveReadyAt)) + } // There is no codepath that updates an item item, alreadyExist := b.bTree.ReplaceOrInsert(item) if alreadyExist { @@ -692,6 +701,7 @@ func TestHighPriorityItemThatBecameReadyIsReturnedBeforeLowPriorityItem(t *testi g.Expect(tickSetup).To(BeClosed()) forwardQueueTimeBy(1 * time.Second) + synctest.Wait() key, prio, _ := q.GetWithPriority() g.Expect(key).To(Equal("prio")) @@ -728,6 +738,36 @@ func TestItemIsReturnedAsSoonAsPossible(t *testing.T) { }) } +func TestWaitingItemIsReturnedRightAfterReadWithoutAfter(t *testing.T) { + t.Parallel() + synctest.Test(t, func(t *testing.T) { + g := NewWithT(t) + + q, metrics := newQueue() + defer q.ShutDown() + + retrieved := make(chan struct{}) + go func() { + item, _, _ := q.GetWithPriority() + g.Expect(item).To(Equal("foo")) + close(retrieved) + }() + synctest.Wait() // Wait for the above goroutine to be blocked + + q.AddWithOpts(AddOpts{After: time.Minute}, "foo") + synctest.Wait() // Wait until the priorityqueue and the above goroutine finish running + + g.Expect(retrieved).ShouldNot(BeClosed()) + + q.AddWithOpts(AddOpts{}, "foo") + synctest.Wait() // Wait until the priorityqueue and the above goroutine finish running + g.Expect(retrieved).Should(BeClosed()) + + g.Expect(metrics.depth["test"]).To(Equal(map[int]int{0: 0})) + g.Expect(metrics.adds["test"]).To(Equal(1)) + }) +} + func TestMultipleItemsWithAfterAreReturnedInCorrectOrder(t *testing.T) { t.Parallel() synctest.Test(t, func(t *testing.T) { From c47f9cb5104b0f46d98f6a2c1a424e7cc654d607 Mon Sep 17 00:00:00 2001 From: zach593 Date: Mon, 12 Jan 2026 23:58:58 +0800 Subject: [PATCH 66/68] Use a buffer to optimize priority queue AddWithOpts performance Signed-off-by: zach593 --- pkg/controller/priorityqueue/priorityqueue.go | 86 +++++++++++++++++-- .../priorityqueue/priorityqueue_test.go | 20 +++-- 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/pkg/controller/priorityqueue/priorityqueue.go b/pkg/controller/priorityqueue/priorityqueue.go index 3fbc127050..fd10a6c050 100644 --- a/pkg/controller/priorityqueue/priorityqueue.go +++ b/pkg/controller/priorityqueue/priorityqueue.go @@ -54,6 +54,11 @@ type Opts[T comparable] struct { // Opt allows to configure a PriorityQueue. type Opt[T comparable] func(*Opts[T]) +type bufferItem[T comparable] struct { + opts AddOpts + items []T +} + // New constructs a new PriorityQueue. func New[T comparable](name string, o ...Opt[T]) PriorityQueue[T] { opts := &Opts[T]{} @@ -70,11 +75,12 @@ func New[T comparable](name string, o ...Opt[T]) PriorityQueue[T] { } pq := &priorityqueue[T]{ - log: opts.Log, - items: map[T]*item[T]{}, - ready: btree.NewG(32, lessReady[T]), - waiting: btree.NewG(32, lessWaiting[T]), - metrics: newQueueMetrics[T](opts.MetricProvider, name, clock.RealClock{}), + log: opts.Log, + itemAddedToAddBuffer: make(chan struct{}, 1), + items: map[T]*item[T]{}, + ready: btree.NewG(32, lessReady[T]), + waiting: btree.NewG(32, lessWaiting[T]), + metrics: newQueueMetrics[T](opts.MetricProvider, name, clock.RealClock{}), // readyItemOrWaiterAdded indicates that a ready item or // waiter was added. It must be buffered, because // if we currently process items we can't tell @@ -89,6 +95,7 @@ func New[T comparable](name string, o ...Opt[T]) PriorityQueue[T] { tick: time.Tick, } + go pq.handleAddBuffer() go pq.handleReadyItems() go pq.handleWaitingItems() go pq.logState() @@ -101,6 +108,11 @@ func New[T comparable](name string, o ...Opt[T]) PriorityQueue[T] { type priorityqueue[T comparable] struct { log logr.Logger + + addBufferLock sync.Mutex + addBuffer []bufferItem[T] + itemAddedToAddBuffer chan struct{} + // lock has to be acquired for any access to any of items, ready, waiting, // addedCounter or waiters. lock sync.Mutex @@ -143,12 +155,53 @@ func (w *priorityqueue[T]) AddWithOpts(o AddOpts, items ...T) { return } + if len(items) == 0 { + return + } + + w.addBufferLock.Lock() + w.addBuffer = append(w.addBuffer, bufferItem[T]{ + opts: o, + items: items, + }) + w.addBufferLock.Unlock() + + w.notifyItemAddedToAddBuffer() +} + +func (w *priorityqueue[T]) handleAddBuffer() { + for { + select { + case <-w.done: + return + case <-w.itemAddedToAddBuffer: + } + + w.lock.Lock() + w.lockedFlushAddBuffer() + w.lock.Unlock() + } +} + +func (w *priorityqueue[T]) lockedFlushAddBuffer() { + w.addBufferLock.Lock() + buffer := w.addBuffer + w.addBuffer = make([]bufferItem[T], 0, len(buffer)) + w.addBufferLock.Unlock() + + for _, v := range buffer { + w.lockedAddWithOpts(v.opts, v.items...) + } +} + +func (w *priorityqueue[T]) lockedAddWithOpts(o AddOpts, items ...T) { + if w.shutdown.Load() { + return + } + var readyItemAdded bool var waitingItemAddedOrUpdated bool - w.lock.Lock() - defer w.lock.Unlock() - for _, key := range items { after := o.After if o.RateLimited { @@ -232,6 +285,13 @@ func (w *priorityqueue[T]) AddWithOpts(o AddOpts, items ...T) { } } +func (w *priorityqueue[T]) notifyItemAddedToAddBuffer() { + select { + case w.itemAddedToAddBuffer <- struct{}{}: + default: + } +} + func (w *priorityqueue[T]) notifyReadyItemOrWaiterAdded() { select { case w.readyItemOrWaiterAdded <- struct{}{}: @@ -309,6 +369,12 @@ func (w *priorityqueue[T]) handleReadyItems() { w.lock.Lock() defer w.lock.Unlock() + // Flush is performed before reading items to avoid errors caused by asynchronous behavior, + // primarily for unit testing purposes. + // Successfully adding a ready item may result in an additional call to handleReadyItems(), + // but the cost is negligible. + w.lockedFlushAddBuffer() + if w.waiters == 0 { return } @@ -424,6 +490,10 @@ func (w *priorityqueue[T]) Len() int { w.lock.Lock() defer w.lock.Unlock() + // Flush is performed before reading items to avoid errors caused by asynchronous behavior, + // primarily for unit testing purposes. + w.lockedFlushAddBuffer() + return w.ready.Len() } diff --git a/pkg/controller/priorityqueue/priorityqueue_test.go b/pkg/controller/priorityqueue/priorityqueue_test.go index 0a1099e053..89c3216fbf 100644 --- a/pkg/controller/priorityqueue/priorityqueue_test.go +++ b/pkg/controller/priorityqueue/priorityqueue_test.go @@ -371,12 +371,20 @@ func BenchmarkAddOnly(b *testing.B) { func BenchmarkAddLockContended(b *testing.B) { q := New[int]("") defer q.ShutDown() - go func() { - for range 1000 { - item, _ := q.Get() - q.Done(item) - } - }() + + for i := range 1000000 { + q.Add(i) + } + + for range 1000 { + go func() { + for { + item, _ := q.Get() + time.Sleep(1 * time.Millisecond) + q.Done(item) + } + }() + } for b.Loop() { for i := range 1000 { From 43b0e35a8e53c7976038c106a5f5e953e64deffd Mon Sep 17 00:00:00 2001 From: GonzaloLuminary <83859776+GonzaloLuminary@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:59:11 +0100 Subject: [PATCH 67/68] =?UTF-8?q?=E2=9C=A8=20Delay=20reconciliation=20unti?= =?UTF-8?q?l=20handlers=20sync=20(#3406)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Delay reconciliation until handlers sync * Address comments * Revert some changes so that the unit tests pass --- pkg/cache/informertest/fake_cache.go | 3 +- pkg/controller/controllertest/util.go | 16 +++- pkg/internal/controller/controller_test.go | 91 ++++++++++++++++++++++ pkg/internal/source/kind.go | 11 ++- 4 files changed, 115 insertions(+), 6 deletions(-) diff --git a/pkg/cache/informertest/fake_cache.go b/pkg/cache/informertest/fake_cache.go index a1a442316f..5f907d774b 100644 --- a/pkg/cache/informertest/fake_cache.go +++ b/pkg/cache/informertest/fake_cache.go @@ -116,7 +116,8 @@ func (c *FakeInformers) informerFor(gvk schema.GroupVersionKind, _ runtime.Objec return informer, nil } - c.InformersByGVK[gvk] = &controllertest.FakeInformer{} + // Set Synced to true by default so that WaitForCacheSync returns immediately + c.InformersByGVK[gvk] = &controllertest.FakeInformer{Synced: true} return c.InformersByGVK[gvk], nil } diff --git a/pkg/controller/controllertest/util.go b/pkg/controller/controllertest/util.go index 2c9a248899..8df24fcf57 100644 --- a/pkg/controller/controllertest/util.go +++ b/pkg/controller/controllertest/util.go @@ -37,6 +37,16 @@ type FakeInformer struct { handlers []cache.ResourceEventHandler } +// fakeHandlerRegistration implements cache.ResourceEventHandlerRegistration for testing. +type fakeHandlerRegistration struct { + informer *FakeInformer +} + +// HasSynced implements cache.ResourceEventHandlerRegistration. +func (f *fakeHandlerRegistration) HasSynced() bool { + return f.informer.Synced +} + // AddIndexers does nothing. TODO(community): Implement this. func (f *FakeInformer) AddIndexers(indexers cache.Indexers) error { return nil @@ -60,19 +70,19 @@ func (f *FakeInformer) HasSynced() bool { // AddEventHandler implements the Informer interface. Adds an EventHandler to the fake Informers. TODO(community): Implement Registration. func (f *FakeInformer) AddEventHandler(handler cache.ResourceEventHandler) (cache.ResourceEventHandlerRegistration, error) { f.handlers = append(f.handlers, handler) - return nil, nil + return &fakeHandlerRegistration{informer: f}, nil } // AddEventHandlerWithResyncPeriod implements the Informer interface. Adds an EventHandler to the fake Informers (ignores resyncPeriod). TODO(community): Implement Registration. func (f *FakeInformer) AddEventHandlerWithResyncPeriod(handler cache.ResourceEventHandler, _ time.Duration) (cache.ResourceEventHandlerRegistration, error) { f.handlers = append(f.handlers, handler) - return nil, nil + return &fakeHandlerRegistration{informer: f}, nil } // AddEventHandlerWithOptions implements the Informer interface. Adds an EventHandler to the fake Informers (ignores options). TODO(community): Implement Registration. func (f *FakeInformer) AddEventHandlerWithOptions(handler cache.ResourceEventHandler, _ cache.HandlerOptions) (cache.ResourceEventHandlerRegistration, error) { f.handlers = append(f.handlers, handler) - return nil, nil + return &fakeHandlerRegistration{informer: f}, nil } // Run implements the Informer interface. Increments f.RunCount. diff --git a/pkg/internal/controller/controller_test.go b/pkg/internal/controller/controller_test.go index 1cef9cc602..90ff30502c 100644 --- a/pkg/internal/controller/controller_test.go +++ b/pkg/internal/controller/controller_test.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "strconv" "sync" "sync/atomic" "time" @@ -386,6 +387,96 @@ var _ = Describe("controller", func() { <-sourceSynced }) + It("should not call Reconcile until all event handlers have processed initial objects", func(specCtx SpecContext) { + nPods := 20 + pods := make([]*corev1.Pod, nPods) + for i := range nPods { + pods[i] = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: strconv.Itoa(i), + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "test", Image: "test"}}, + }, + } + _, err := clientset.CoreV1().Pods("default").Create(specCtx, pods[i], metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + } + defer func() { + for _, pod := range pods { + _ = clientset.CoreV1().Pods("default").Delete(specCtx, pod.Name, metav1.DeleteOptions{}) + } + }() + + testCache, err := cache.New(cfg, cache.Options{}) + Expect(err).NotTo(HaveOccurred()) + + ctx, cancel := context.WithCancel(specCtx) + defer cancel() + go func() { + defer GinkgoRecover() + _ = testCache.Start(ctx) + }() + + // Tracks how many objects have been processed by the event handler. + var handlerProcessedCount atomic.Int32 + + // Channel to block one of the event handlers to simulate slow event handler processing. + blockHandler := make(chan struct{}) + + // Tracks whether Reconcile was called. + var reconcileCalled atomic.Bool + + // Create the controller. + testCtrl := New(Options[reconcile.Request]{ + MaxConcurrentReconciles: 1, + CacheSyncTimeout: 10 * time.Second, + NewQueue: func(string, workqueue.TypedRateLimiter[reconcile.Request]) workqueue.TypedRateLimitingInterface[reconcile.Request] { + return &controllertest.Queue{ + TypedInterface: workqueue.NewTyped[reconcile.Request](), + } + }, + Name: "test-reconcile-order", + LogConstructor: func(_ *reconcile.Request) logr.Logger { + return log.RuntimeLog.WithName("test-reconcile-order") + }, + Do: reconcile.Func(func(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + // handlerProcessedCount should be equal to the number of pods created since we are waiting + // for the handlers to finish processing before reconciling. + Expect(handlerProcessedCount.Load()).To(Equal(int32(nPods))) + reconcileCalled.Store(true) + return reconcile.Result{}, nil + })}, + ) + + err = testCtrl.Watch(source.Kind(testCache, &corev1.Pod{}, handler.TypedFuncs[*corev1.Pod, reconcile.Request]{ + CreateFunc: func(ctx context.Context, evt event.TypedCreateEvent[*corev1.Pod], q workqueue.TypedRateLimitingInterface[reconcile.Request]) { + <-blockHandler + handlerProcessedCount.Add(1) + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: evt.Object.GetName(), Namespace: evt.Object.GetNamespace()}}) + }, + })) + Expect(err).NotTo(HaveOccurred()) + + controllerDone := make(chan error) + go func() { + defer GinkgoRecover() + controllerDone <- testCtrl.Start(ctx) + }() + + // Give the controller time to start the reconciler. We asserts + // in there that all events have been processed, so if we start it + // prematurely, that assertion will fail. We can not get rid of the + // sleep unless we stop using envtest for this test. + time.Sleep(1 * time.Second) + close(blockHandler) + Eventually(reconcileCalled.Load).Should(BeTrue()) + + cancel() + Eventually(controllerDone, 5*time.Second).Should(Receive()) + }) + It("should process events from source.Channel", func(ctx SpecContext) { ctrl.CacheSyncTimeout = 10 * time.Second // channel to be closed when event is processed diff --git a/pkg/internal/source/kind.go b/pkg/internal/source/kind.go index 2854244523..a28aeb177e 100644 --- a/pkg/internal/source/kind.go +++ b/pkg/internal/source/kind.go @@ -91,16 +91,23 @@ func (ks *Kind[object, request]) Start(ctx context.Context, queue workqueue.Type return } - _, err := i.AddEventHandlerWithOptions(NewEventHandler(ctx, queue, ks.Handler, ks.Predicates), toolscache.HandlerOptions{ + handlerRegistration, err := i.AddEventHandlerWithOptions(NewEventHandler(ctx, queue, ks.Handler, ks.Predicates), toolscache.HandlerOptions{ Logger: &logKind, }) if err != nil { ks.startedErr <- err return } + // First, wait for the cache to sync. For real caches this waits for startup. + // For fakes with Synced=false, this returns immediately allowing fast failure. if !ks.Cache.WaitForCacheSync(ctx) { - // Would be great to return something more informative here ks.startedErr <- errors.New("cache did not sync") + close(ks.startedErr) + return + } + // Then wait for this specific handler to receive all initial events. + if !toolscache.WaitForCacheSync(ctx.Done(), handlerRegistration.HasSynced) { + ks.startedErr <- errors.New("handler did not sync") } close(ks.startedErr) }() From 00b8b07cf3445ce3a46bd6c30430401814311392 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sat, 17 Jan 2026 13:32:15 -0500 Subject: [PATCH 68/68] :bug: Limit depthWithPriorityMetric cardinality to 25 If a very large number of priorities is used, the cardinality of the priorityQueues `depthWithPriority` metric can grow up to the size of int, which is unreasonable high. As the functionality is useful and the above is an outlier use-case, make the metric "smart" by collecting the priorities up to a cardinality of 25 unique values and then start using a placeholder that is hopefully self-explanatory. --- pkg/internal/metrics/workqueue.go | 48 ++++++- pkg/internal/metrics/workqueue_suite_test.go | 29 ++++ pkg/internal/metrics/workqueue_test.go | 134 +++++++++++++++++++ 3 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 pkg/internal/metrics/workqueue_suite_test.go create mode 100644 pkg/internal/metrics/workqueue_test.go diff --git a/pkg/internal/metrics/workqueue.go b/pkg/internal/metrics/workqueue.go index 402319817b..49180457a6 100644 --- a/pkg/internal/metrics/workqueue.go +++ b/pkg/internal/metrics/workqueue.go @@ -18,9 +18,11 @@ package metrics import ( "strconv" + "sync" "time" "github.com/prometheus/client_golang/prometheus" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/metrics" ) @@ -154,17 +156,55 @@ type DepthMetricWithPriority interface { var _ MetricsProviderWithPriority = WorkqueueMetricsProvider{} func (WorkqueueMetricsProvider) NewDepthMetricWithPriority(name string) DepthMetricWithPriority { - return &depthWithPriorityMetric{lvs: []string{name, name}} + return &depthWithPriorityMetric{depth: depth, lvs: []string{name, name}, observedPriorities: sets.Set[int]{}} } +type prometheusGaugeVec interface { + WithLabelValues(lvs ...string) prometheus.Gauge +} + +const ( + priorityCardinalityExceededPlaceholder = "exceeded_cardinality_limit" + // maxRecommendedUniquePriorities is not scientifically chosen, we assume + // that the 99% use-case is to only use the two priorities that c-r itself + // uses and then leave a bit of leeway for other use-cases. + // We may decide to update this value in the future if we find that a + // a different value is more appropriate. + maxRecommendedUniquePriorities = 25 +) + type depthWithPriorityMetric struct { - lvs []string + depth prometheusGaugeVec + lvs []string + + observedPrioritiesLock sync.Mutex + priorityCardinalityLimitReached bool + observedPriorities sets.Set[int] +} + +func (g *depthWithPriorityMetric) priorityLabel(priority int) string { + g.observedPrioritiesLock.Lock() + defer g.observedPrioritiesLock.Unlock() + + if g.priorityCardinalityLimitReached { + return priorityCardinalityExceededPlaceholder + } + + g.observedPriorities.Insert(priority) + + if g.observedPriorities.Len() > maxRecommendedUniquePriorities { + g.observedPriorities = nil + g.priorityCardinalityLimitReached = true + return priorityCardinalityExceededPlaceholder + } + + return strconv.Itoa(priority) } func (g *depthWithPriorityMetric) Inc(priority int) { - depth.WithLabelValues(append(g.lvs, strconv.Itoa(priority))...).Inc() + g.depth.WithLabelValues(append(g.lvs, g.priorityLabel(priority))...).Inc() } func (g *depthWithPriorityMetric) Dec(priority int) { - depth.WithLabelValues(append(g.lvs, strconv.Itoa(priority))...).Dec() + g.depth.WithLabelValues(append(g.lvs, g.priorityLabel(priority))...).Dec() } diff --git a/pkg/internal/metrics/workqueue_suite_test.go b/pkg/internal/metrics/workqueue_suite_test.go new file mode 100644 index 0000000000..d647cf64bf --- /dev/null +++ b/pkg/internal/metrics/workqueue_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestWorkqueue(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Workqueue Metrics Suite") +} diff --git a/pkg/internal/metrics/workqueue_test.go b/pkg/internal/metrics/workqueue_test.go new file mode 100644 index 0000000000..c3cb7378e7 --- /dev/null +++ b/pkg/internal/metrics/workqueue_test.go @@ -0,0 +1,134 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "sync" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/prometheus/client_golang/prometheus" + "k8s.io/apimachinery/pkg/util/sets" +) + +type fakeGauge struct { + prometheus.Gauge + inc func() +} + +func (f *fakeGauge) Inc() { f.inc() } + +type fakeGaugeVec struct { + gauges map[string]int + mu sync.Mutex +} + +func (f *fakeGaugeVec) WithLabelValues(lvs ...string) prometheus.Gauge { + f.mu.Lock() + defer f.mu.Unlock() + + key := "" + for _, lv := range lvs { + key += lv + "|" + } + if _, ok := f.gauges[key]; !ok { + f.gauges[key] = 0 + } + return &fakeGauge{inc: func() { + f.mu.Lock() + f.gauges[key]++ + f.mu.Unlock() + }} +} + +var _ = Describe("depthWithPriorityMetric", func() { + Describe("CardinalityLimit", func() { + type testCase struct { + numUniquePriorities int + expectedKeyCount int + expectedCardinalityExceededVal int + } + + DescribeTable("should respect cardinality limits", + func(tc testCase) { + fakeVec := &fakeGaugeVec{gauges: make(map[string]int)} + m := &depthWithPriorityMetric{ + depth: fakeVec, + lvs: []string{"test", "test"}, + observedPriorities: sets.Set[int]{}, + } + + wg := &sync.WaitGroup{} + wg.Add(tc.numUniquePriorities) + for i := 1; i <= tc.numUniquePriorities; i++ { + go func() { + m.Inc(i) + wg.Done() + }() + } + wg.Wait() + + Expect(fakeVec.gauges).To(HaveLen(tc.expectedKeyCount)) + + placeholderKey := "test|test|" + priorityCardinalityExceededPlaceholder + "|" + Expect(fakeVec.gauges[placeholderKey]).To(Equal(tc.expectedCardinalityExceededVal)) + }, + Entry("under limit does not use placeholder", testCase{ + numUniquePriorities: 10, + expectedKeyCount: 10, + expectedCardinalityExceededVal: 0, + }), + Entry("at limit does not use placeholder", testCase{ + numUniquePriorities: 25, + expectedKeyCount: 25, + expectedCardinalityExceededVal: 0, + }), + Entry("exceeding limit uses placeholder", testCase{ + numUniquePriorities: 26, + expectedKeyCount: 26, + expectedCardinalityExceededVal: 1, + }), + Entry("well over limit uses placeholder for all excess", testCase{ + numUniquePriorities: 30, + expectedKeyCount: 26, + expectedCardinalityExceededVal: 5, + }), + ) + }) + + It("same priority many adds does not trigger cardinality limit", func() { + fakeVec := &fakeGaugeVec{gauges: make(map[string]int)} + m := &depthWithPriorityMetric{ + depth: fakeVec, + lvs: []string{"test", "test"}, + observedPriorities: sets.Set[int]{}, + } + + wg := &sync.WaitGroup{} + wg.Add(200) + for range 200 { + go func() { + m.Inc(1) + wg.Done() + }() + } + wg.Wait() + + Expect(fakeVec.gauges).To(HaveLen(1)) + Expect(fakeVec.gauges["test|test|1|"]).To(Equal(200)) + }) +})