From 9965f5bfe4bb5eaf745e465a3ab2d410fe1d519b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:22:42 +0000 Subject: [PATCH] build(deps): Bump sigs.k8s.io/controller-runtime from 0.24.1 to 0.25.0 Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.24.1 to 0.25.0. - [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases) - [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.24.1...v0.25.0) --- updated-dependencies: - dependency-name: sigs.k8s.io/controller-runtime dependency-version: 0.25.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 +- vendor/modules.txt | 5 +- .../controller-runtime/.golangci.yml | 6 +- vendor/sigs.k8s.io/controller-runtime/FAQ.md | 7 +- .../sigs.k8s.io/controller-runtime/Makefile | 26 +- .../sigs.k8s.io/controller-runtime/README.md | 2 + .../sigs.k8s.io/controller-runtime/alias.go | 4 +- .../controller-runtime/pkg/cache/cache.go | 71 +-- .../pkg/cache/cacheapi/cacheapi.go | 153 ++++++ .../pkg/cache/delegating_by_gvk_cache.go | 19 +- .../pkg/cache/informer_cache.go | 3 +- .../pkg/cache/internal/cache_reader.go | 7 +- .../pkg/cache/internal/informers.go | 6 +- .../pkg/cache/multi_namespace_cache.go | 39 +- .../controller-runtime/pkg/client/client.go | 87 ++- .../pkg/client/config/config.go | 6 +- .../pkg/client/consistency.go | 494 ++++++++++++++++++ .../pkg/client/fake/client.go | 190 +++++-- .../pkg/client/fake/versioned_tracker.go | 59 ++- .../pkg/client/interceptor/intercept.go | 23 + .../pkg/client/interfaces.go | 15 +- .../consistencyhandler/consistencyhandler.go | 345 ++++++++++++ .../internal/writebarrier/writebarrier.go | 195 +++++++ .../pkg/client/metadata_client.go | 18 - .../controller-runtime/pkg/client/object.go | 7 +- .../controller-runtime/pkg/client/options.go | 124 +++++ .../pkg/client/typed_client.go | 25 +- .../pkg/client/unstructured_client.go | 15 +- .../controller-runtime/pkg/client/watch.go | 17 +- .../pkg/cluster/internal.go | 4 +- .../pkg/config/controller.go | 9 +- .../pkg/controller/controller.go | 21 +- .../pkg/controller/priorityqueue/metrics.go | 16 + .../controller/priorityqueue/priorityqueue.go | 27 +- .../controller-runtime/pkg/handler/enqueue.go | 2 +- .../pkg/internal/controller/controller.go | 5 +- .../internal/controller/metrics/metrics.go | 2 +- .../pkg/internal/field/selector/utils.go | 2 +- .../pkg/internal/httpserver/server.go | 16 + .../pkg/internal/metrics/workqueue.go | 2 +- .../pkg/internal/recorder/recorder.go | 31 +- .../pkg/internal/source/event_handler.go | 4 +- .../pkg/internal/source/kind.go | 18 +- .../pkg/internal/syncs/syncs.go | 16 + .../pkg/manager/internal.go | 8 +- .../pkg/manager/runnable_group.go | 16 + .../controller-runtime/pkg/manager/server.go | 2 +- .../pkg/metrics/client_go_adapter.go | 270 +++++++++- .../pkg/metrics/leaderelection.go | 16 + .../pkg/predicate/predicate.go | 5 +- .../pkg/recorder/recorder.go | 19 +- .../controller-runtime/pkg/source/source.go | 16 +- .../pkg/webhook/admission/decode.go | 2 +- .../pkg/webhook/admission/defaulter_custom.go | 2 +- .../pkg/webhook/admission/multi.go | 2 +- .../pkg/webhook/admission/response.go | 2 +- .../pkg/webhook/admission/validator_custom.go | 2 +- .../pkg/webhook/admission/webhook.go | 2 +- .../controller-runtime/pkg/webhook/server.go | 28 +- 60 files changed, 2246 insertions(+), 295 deletions(-) create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/cache/cacheapi/cacheapi.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/client/consistency.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/client/internal/consistencyhandler/consistencyhandler.go create mode 100644 vendor/sigs.k8s.io/controller-runtime/pkg/client/internal/writebarrier/writebarrier.go diff --git a/go.mod b/go.mod index 3cf13955f4..c6dfbc7587 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( k8s.io/client-go v0.37.0 k8s.io/klog/v2 v2.140.0 k8s.io/utils v0.0.0-20260626114624-be93311217bd - sigs.k8s.io/controller-runtime v0.24.1 + sigs.k8s.io/controller-runtime v0.25.0 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index 3237995b39..6e0392c257 100644 --- a/go.sum +++ b/go.sum @@ -300,8 +300,8 @@ k8s.io/kubectl v0.36.0 h1:hEGr8NvIm2Wjqs2Xy48Uzmvo6lpHdGKlLyMvau2gTms= k8s.io/kubectl v0.36.0/go.mod h1:iDe8aV5BEi45W8k+5n71I2pJ/nwE0PHDu+/2cejzYoo= k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs= k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= -sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/controller-runtime v0.25.0 h1:44KgRUPew331KSJpNu8zJow3iTR5W0p/SfrHdw3lV40= +sigs.k8s.io/controller-runtime v0.25.0/go.mod h1:4QqLdT6z/L6Olj8JJCtvztid4/fnIiYsfaTFScegctc= 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/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= diff --git a/vendor/modules.txt b/vendor/modules.txt index 84185b6eae..9918599978 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1066,11 +1066,12 @@ k8s.io/utils/net k8s.io/utils/ptr k8s.io/utils/third_party/forked/golang/btree k8s.io/utils/trace -# sigs.k8s.io/controller-runtime v0.24.1 +# sigs.k8s.io/controller-runtime v0.25.0 ## explicit; go 1.26.0 sigs.k8s.io/controller-runtime sigs.k8s.io/controller-runtime/pkg/builder sigs.k8s.io/controller-runtime/pkg/cache +sigs.k8s.io/controller-runtime/pkg/cache/cacheapi sigs.k8s.io/controller-runtime/pkg/cache/internal sigs.k8s.io/controller-runtime/pkg/certwatcher sigs.k8s.io/controller-runtime/pkg/certwatcher/metrics @@ -1079,6 +1080,8 @@ sigs.k8s.io/controller-runtime/pkg/client/apiutil sigs.k8s.io/controller-runtime/pkg/client/config sigs.k8s.io/controller-runtime/pkg/client/fake sigs.k8s.io/controller-runtime/pkg/client/interceptor +sigs.k8s.io/controller-runtime/pkg/client/internal/consistencyhandler +sigs.k8s.io/controller-runtime/pkg/client/internal/writebarrier sigs.k8s.io/controller-runtime/pkg/cluster sigs.k8s.io/controller-runtime/pkg/config sigs.k8s.io/controller-runtime/pkg/controller diff --git a/vendor/sigs.k8s.io/controller-runtime/.golangci.yml b/vendor/sigs.k8s.io/controller-runtime/.golangci.yml index 334333a5f2..669ff5aca7 100644 --- a/vendor/sigs.k8s.io/controller-runtime/.golangci.yml +++ b/vendor/sigs.k8s.io/controller-runtime/.golangci.yml @@ -56,6 +56,9 @@ linters: msg: Use ginkgos SpecContext or go testings t.Context instead - pattern: context.TODO msg: Use ginkgos SpecContext or go testings t.Context instead + goconst: + ignore-tests: true + min-occurrences: 5 govet: disable: - fieldalignment @@ -77,10 +80,11 @@ linters: - pkg: sigs.k8s.io/controller-runtime alias: ctrl no-unaliased: true + misspell: + mode: default modernize: disable: - omitzero - - fmtappendf revive: rules: # The following rules are recommended https://github.com/mgechev/revive#recommended-configuration diff --git a/vendor/sigs.k8s.io/controller-runtime/FAQ.md b/vendor/sigs.k8s.io/controller-runtime/FAQ.md index 9c36c8112e..c182755349 100644 --- a/vendor/sigs.k8s.io/controller-runtime/FAQ.md +++ b/vendor/sigs.k8s.io/controller-runtime/FAQ.md @@ -14,7 +14,7 @@ attempt to reconcile *all* state for that given root objects. should always reconcile state by reading all the state it needs, then writing updates. This allows your reconciler to correctly respond to generic events, adjust to skipped or coalesced events, and easily deal -with application startup. The controller will enqueue reconcile requests +with application startup. The controller will enqueue reconcile requests for both old and new objects if a mapping changes, but it's your responsibility to make sure you have enough information to be able clean up state that's no longer referenced. @@ -40,10 +40,7 @@ on your situation. In general, write your controller with the assumption that information will eventually be correct, but may be slightly out of date. Make sure that your reconcile function enforces the entire state of the world each -time it runs. If none of this works for you, you can always construct -a client that reads directly from the API server, but this is generally -considered to be a last resort, and the two approaches above should -generally cover most circumstances. +time it runs. ### Q: Where's the fake client? How do I use it? diff --git a/vendor/sigs.k8s.io/controller-runtime/Makefile b/vendor/sigs.k8s.io/controller-runtime/Makefile index 40e7db6e70..f64d647e2a 100644 --- a/vendor/sigs.k8s.io/controller-runtime/Makefile +++ b/vendor/sigs.k8s.io/controller-runtime/Makefile @@ -1,18 +1,16 @@ -#!/usr/bin/env bash - -# Copyright 2020 The Kubernetes Authors. +# Copyright 2020 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 +# 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 +# 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. +# 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. # If you update this file, please follow # https://suva.sh/posts/well-documented-makefiles @@ -198,6 +196,10 @@ verify-modules: modules $(GO_MOD_CHECK) ## Verify go modules are up to date fi $(GO_MOD_CHECK) $(GO_MOD_CHECK_IGNORE) +.PHONY: verify-boilerplate +verify-boilerplate: + ./hack/verify-boilerplate.sh + APIDIFF_OLD_COMMIT ?= $(shell git rev-parse origin/main) .PHONY: apidiff diff --git a/vendor/sigs.k8s.io/controller-runtime/README.md b/vendor/sigs.k8s.io/controller-runtime/README.md index 8549f4e880..35eddc4574 100644 --- a/vendor/sigs.k8s.io/controller-runtime/README.md +++ b/vendor/sigs.k8s.io/controller-runtime/README.md @@ -53,6 +53,8 @@ Compatible k8s.io/*, client-go and minimum Go versions can be looked up in our [ | | k8s.io/*, client-go | minimum Go version | |----------|:-------------------:|:------------------:| +| CR v0.24 | v0.36 | 1.26 | +| CR v0.23 | v0.35 | 1.25 | | CR v0.22 | v0.34 | 1.24 | | CR v0.21 | v0.33 | 1.24 | | CR v0.20 | v0.32 | 1.23 | diff --git a/vendor/sigs.k8s.io/controller-runtime/alias.go b/vendor/sigs.k8s.io/controller-runtime/alias.go index a2848693a2..3077a9f4dc 100644 --- a/vendor/sigs.k8s.io/controller-runtime/alias.go +++ b/vendor/sigs.k8s.io/controller-runtime/alias.go @@ -107,7 +107,7 @@ var ( // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // - // The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and + // The returned `*rest.Config` has client-side rate limiting 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. // // Will log an error and exit if there is an error creating the rest.Config. @@ -117,7 +117,7 @@ var ( // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // - // The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and + // The returned `*rest.Config` has client-side rate limiting 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. // // Config precedence diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cache.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cache.go index 2a6a6c6be0..73026259b7 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cache.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cache.go @@ -17,7 +17,6 @@ limitations under the License. package cache import ( - "context" "fmt" "maps" "net/http" @@ -36,6 +35,7 @@ import ( toolscache "k8s.io/client-go/tools/cache" "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/cache/cacheapi" "sigs.k8s.io/controller-runtime/pkg/cache/internal" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" @@ -46,17 +46,15 @@ var ( ) // InformerGetOptions defines the behavior of how informers are retrieved. -type InformerGetOptions internal.GetOptions +type InformerGetOptions = cacheapi.InformerGetOptions // InformerGetOption defines an option that alters the behavior of how informers are retrieved. -type InformerGetOption func(*InformerGetOptions) +type InformerGetOption = cacheapi.InformerGetOption // BlockUntilSynced determines whether a get request for an informer should block // until the informer's cache has synced. func BlockUntilSynced(shouldBlock bool) InformerGetOption { - return func(opts *InformerGetOptions) { - opts.BlockUntilSynced = &shouldBlock - } + return cacheapi.BlockUntilSynced(shouldBlock) } // Cache knows how to load Kubernetes objects, fetch informers to request @@ -73,67 +71,10 @@ type Cache interface { // Informers knows how to create or fetch informers for different // group-version-kinds, and add indices to those informers. It's safe to call // GetInformer from multiple threads. -type Informers interface { - // GetInformer fetches or constructs an informer for the given object that corresponds to a single - // API kind and resource. - GetInformer(ctx context.Context, obj client.Object, opts ...InformerGetOption) (Informer, error) - - // GetInformerForKind is similar to GetInformer, except that it takes a group-version-kind, instead - // of the underlying object. - GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind, opts ...InformerGetOption) (Informer, error) - - // RemoveInformer removes an informer entry and stops it if it was running. - RemoveInformer(ctx context.Context, obj client.Object) error - - // Start runs all the informers known to this cache until the context is closed. - // It blocks. - Start(ctx context.Context) error - - // WaitForCacheSync waits for all the caches to sync. Returns false if it could not sync a cache. - WaitForCacheSync(ctx context.Context) bool - - // FieldIndexer adds indices to the managed informers. - client.FieldIndexer -} +type Informers = cacheapi.Informers // Informer allows you to interact with the underlying informer. -type Informer interface { - // AddEventHandler adds an event handler to the shared informer using the shared informer's resync - // period. Events to a single handler are delivered sequentially, but there is no coordination - // between different handlers. - // It returns a registration handle for the handler that can be used to remove - // the handler again and an error if the handler cannot be added. - AddEventHandler(handler toolscache.ResourceEventHandler) (toolscache.ResourceEventHandlerRegistration, error) - - // AddEventHandlerWithResyncPeriod adds an event handler to the shared informer using the - // specified resync period. Events to a single handler are delivered sequentially, but there is - // no coordination between different handlers. - // It returns a registration handle for the handler that can be used to remove - // the handler again and an error if the handler cannot be added. - AddEventHandlerWithResyncPeriod(handler toolscache.ResourceEventHandler, resyncPeriod time.Duration) (toolscache.ResourceEventHandlerRegistration, error) - - // AddEventHandlerWithOptions is a variant of AddEventHandlerWithResyncPeriod where - // all optional parameters are passed in as a struct. - AddEventHandlerWithOptions(handler toolscache.ResourceEventHandler, options toolscache.HandlerOptions) (toolscache.ResourceEventHandlerRegistration, error) - - // RemoveEventHandler removes a previously added event handler given by - // its registration handle. - // This function is guaranteed to be idempotent and thread-safe. - RemoveEventHandler(handle toolscache.ResourceEventHandlerRegistration) error - - // AddIndexers adds indexers to this store. It is valid to add indexers - // after an informer was started. - AddIndexers(indexers toolscache.Indexers) error - - // HasSynced return true if the informers underlying store has synced. - HasSynced() bool - - // HasSyncedChecker completes if the informers underlying store has synced. - HasSyncedChecker() toolscache.DoneChecker - - // IsStopped returns true if the informer has been stopped. - IsStopped() bool -} +type Informer = cacheapi.Informer // AllNamespaces should be used as the map key to deliminate namespace settings // that apply to all namespaces that themselves do not have explicit settings. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cacheapi/cacheapi.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cacheapi/cacheapi.go new file mode 100644 index 0000000000..9f3aa62eeb --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/cacheapi/cacheapi.go @@ -0,0 +1,153 @@ +/* +Copyright 2026 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 cacheapi + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + toolscache "k8s.io/client-go/tools/cache" +) + +// InformerGetOptions defines the behavior of how informers are retrieved. +type InformerGetOptions struct { + // BlockUntilSynced controls if the informer retrieval will block until the informer is synced. Defaults to `true`. + BlockUntilSynced *bool +} + +// InformerGetOption defines an option that alters the behavior of how informers are retrieved. +type InformerGetOption func(*InformerGetOptions) + +// BlockUntilSynced determines whether a get request for an informer should block +// until the informer's cache has synced. +func BlockUntilSynced(shouldBlock bool) InformerGetOption { + return func(opts *InformerGetOptions) { + opts.BlockUntilSynced = &shouldBlock + } +} + +// Object is a Kubernetes object, allows functions to work indistinctly with +// any resource that implements both Object interfaces. +// +// Semantically, these are objects which are both serializable (runtime.Object) +// and identifiable (metav1.Object) -- think any object which you could write +// as YAML or JSON, and then `kubectl create`. +// +// Code-wise, this means that any object which embeds both ObjectMeta (which +// provides metav1.Object) and TypeMeta (which provides half of runtime.Object) +// and has a `DeepCopyObject` implementation (the other half of runtime.Object) +// will implement this by default. +// +// For example, nearly all the built-in types are Objects, as well as all +// KubeBuilder-generated CRDs (unless you do something real funky to them). +// +// By and large, most things that implement runtime.Object also implement +// Object -- it's very rare to have *just* a runtime.Object implementation (the +// cases tend to be funky built-in types like Webhook payloads that don't have +// a `metadata` field). +// +// Notice that XYZList types are distinct: they implement ObjectList instead. +type Object interface { + metav1.Object + runtime.Object +} + +// IndexerFunc knows how to take an object and turn it into a series +// of non-namespaced keys. Namespaced objects are automatically given +// namespaced and non-spaced variants, so keys do not need to include namespace. +type IndexerFunc func(Object) []string + +// FieldIndexer knows how to index over a particular "field" such that it +// can later be used by a field selector. +type FieldIndexer interface { + // IndexField adds an index with the given field name on the given object type + // by using the given function to extract the value for that field. If you want + // compatibility with the Kubernetes API server, only return one key, and only use + // fields that the API server supports. Otherwise, you can return multiple keys, + // and "equality" in the field selector means that at least one key matches the value. + // The FieldIndexer will automatically take care of indexing over namespace + // and supporting efficient all-namespace queries. + IndexField(ctx context.Context, obj Object, field string, extractValue IndexerFunc) error +} + +// Informers knows how to create or fetch informers for different +// group-version-kinds, and add indices to those informers. It's safe to call +// GetInformer from multiple threads. +type Informers interface { + // GetInformer fetches or constructs an informer for the given object that corresponds to a single + // API kind and resource. + GetInformer(ctx context.Context, obj Object, opts ...InformerGetOption) (Informer, error) + + // GetInformerForKind is similar to GetInformer, except that it takes a group-version-kind, instead + // of the underlying object. + GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind, opts ...InformerGetOption) (Informer, error) + + // RemoveInformer removes an informer entry and stops it if it was running. + RemoveInformer(ctx context.Context, obj Object) error + + // Start runs all the informers known to this cache until the context is closed. + // It blocks. + Start(ctx context.Context) error + + // WaitForCacheSync waits for all the caches to sync. Returns false if it could not sync a cache. + WaitForCacheSync(ctx context.Context) bool + + // FieldIndexer adds indices to the managed informers. + FieldIndexer +} + +// Informer allows you to interact with the underlying informer. +type Informer interface { + // AddEventHandler adds an event handler to the shared informer using the shared informer's resync + // period. Events to a single handler are delivered sequentially, but there is no coordination + // between different handlers. + // It returns a registration handle for the handler that can be used to remove + // the handler again and an error if the handler cannot be added. + AddEventHandler(handler toolscache.ResourceEventHandler) (toolscache.ResourceEventHandlerRegistration, error) + + // AddEventHandlerWithResyncPeriod adds an event handler to the shared informer using the + // specified resync period. Events to a single handler are delivered sequentially, but there is + // no coordination between different handlers. + // It returns a registration handle for the handler that can be used to remove + // the handler again and an error if the handler cannot be added. + AddEventHandlerWithResyncPeriod(handler toolscache.ResourceEventHandler, resyncPeriod time.Duration) (toolscache.ResourceEventHandlerRegistration, error) + + // AddEventHandlerWithOptions is a variant of AddEventHandlerWithResyncPeriod where + // all optional parameters are passed in as a struct. + AddEventHandlerWithOptions(handler toolscache.ResourceEventHandler, options toolscache.HandlerOptions) (toolscache.ResourceEventHandlerRegistration, error) + + // RemoveEventHandler removes a previously added event handler given by + // its registration handle. + // This function is guaranteed to be idempotent and thread-safe. + RemoveEventHandler(handle toolscache.ResourceEventHandlerRegistration) error + + // AddIndexers adds indexers to this store. It is valid to add indexers + // after an informer was started. + AddIndexers(indexers toolscache.Indexers) error + + // HasSynced return true if the informers underlying store has synced. + HasSynced() bool + + // HasSyncedChecker completes if the informers underlying store has synced. + HasSyncedChecker() toolscache.DoneChecker + + // IsStopped returns true if the informer has been stopped. + IsStopped() bool +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/delegating_by_gvk_cache.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/delegating_by_gvk_cache.go index adc5d957a4..faeb55454b 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/delegating_by_gvk_cache.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/delegating_by_gvk_cache.go @@ -21,8 +21,8 @@ import ( "maps" "slices" "strings" - "sync" + "golang.org/x/sync/errgroup" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" @@ -77,24 +77,15 @@ func (dbt *delegatingByGVKCache) Start(ctx context.Context) error { allCaches := slices.Collect(maps.Values(dbt.caches)) allCaches = append(allCaches, dbt.defaultCache) - wg := &sync.WaitGroup{} - errs := make(chan error) + group, childCtx := errgroup.WithContext(ctx) for idx := range allCaches { cache := allCaches[idx] - wg.Go(func() { - if err := cache.Start(ctx); err != nil { - errs <- err - } + group.Go(func() error { + return cache.Start(childCtx) }) } - select { - case err := <-errs: - return err - case <-ctx.Done(): - wg.Wait() - return nil - } + return ignoreContextCanceled(group.Wait()) } func (dbt *delegatingByGVKCache) WaitForCacheSync(ctx context.Context) bool { diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/informer_cache.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/informer_cache.go index f8a1faa7b9..13ac80e75c 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/informer_cache.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/informer_cache.go @@ -76,6 +76,7 @@ func (ic *informerCache) Get(ctx context.Context, key client.ObjectKey, out clie if !started { return &ErrCacheNotStarted{} } + return cache.Reader.Get(ctx, key, out, opts...) } @@ -137,7 +138,7 @@ func applyGetOptions(opts ...InformerGetOption) *internal.GetOptions { for _, opt := range opts { opt(cfg) } - return (*internal.GetOptions)(cfg) + return cfg } // GetInformerForKind returns the informer for the GroupVersionKind. If no informer exists, one will be started. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/cache_reader.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/cache_reader.go index 624869f590..3c4ea50c97 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/cache_reader.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/cache_reader.go @@ -34,9 +34,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/internal/field/selector" ) -// CacheReader is a client.Reader. -var _ client.Reader = &CacheReader{} - // CacheReader wraps a cache.Index to implement the client.Reader interface for a single type. type CacheReader struct { // indexer is the underlying indexer wrapped by this cache. @@ -55,7 +52,7 @@ type CacheReader struct { } // Get checks the indexer for the object and writes a copy of it if found. -func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Object, opts ...client.GetOption) error { +func (c *CacheReader) Get(ctx context.Context, key client.ObjectKey, out client.Object, opts ...client.GetOption) error { getOpts := client.GetOptions{} getOpts.ApplyOptions(opts) @@ -109,7 +106,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 { +func (c *CacheReader) List(ctx context.Context, out client.ObjectList, opts ...client.ListOption) error { var objs []any var err error diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/informers.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/informers.go index 619e36abd3..4724e26e9b 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/informers.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/internal/informers.go @@ -38,6 +38,7 @@ import ( "k8s.io/client-go/metadata" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/controller-runtime/pkg/cache/cacheapi" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" logf "sigs.k8s.io/controller-runtime/pkg/internal/log" "sigs.k8s.io/controller-runtime/pkg/internal/syncs" @@ -136,10 +137,7 @@ type tracker struct { // GetOptions provides configuration to customize the behavior when // getting an informer. -type GetOptions struct { - // BlockUntilSynced controls if the informer retrieval will block until the informer is synced. Defaults to `true`. - BlockUntilSynced *bool -} +type GetOptions = cacheapi.InformerGetOptions // Informers create and caches Informers for (runtime.Object, schema.GroupVersionKind) pairs. // It uses a standard parameter codec constructed based on the given generated Scheme. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/multi_namespace_cache.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/multi_namespace_cache.go index 592519c35d..05fdcb8655 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cache/multi_namespace_cache.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cache/multi_namespace_cache.go @@ -18,10 +18,12 @@ package cache import ( "context" + "errors" "fmt" "strings" "time" + "golang.org/x/sync/errgroup" corev1 "k8s.io/api/core/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -160,31 +162,34 @@ func (c *multiNamespaceCache) GetInformerForKind(ctx context.Context, gvk schema } func (c *multiNamespaceCache) Start(ctx context.Context) error { - errs := make(chan error) - // start global cache + group, childCtx := errgroup.WithContext(ctx) + if c.clusterCache != nil { - go func() { - err := c.clusterCache.Start(ctx) - if err != nil { - errs <- fmt.Errorf("failed to start cluster-scoped cache: %w", err) - } - }() + clusterCache := c.clusterCache + group.Go(func() error { + return clusterCache.Start(childCtx) + }) } - // start namespaced caches for ns, cache := range c.namespaceToCache { - go func(ns string, cache Cache) { - if err := cache.Start(ctx); err != nil { - errs <- fmt.Errorf("failed to start cache for namespace %s: %w", ns, err) + group.Go(func() error { + if err := cache.Start(childCtx); err != nil { + return fmt.Errorf("failed to start cache for namespace %s: %w", ns, err) } - }(ns, cache) + return nil + }) } - select { - case <-ctx.Done(): + + return ignoreContextCanceled(group.Wait()) +} + +// ignoreContextCanceled returns nil if the error is a context.Canceled error, +// otherwise returns the error unchanged. +func ignoreContextCanceled(err error) error { + if errors.Is(err, context.Canceled) { return nil - case err := <-errs: - return err } + return err } func (c *multiNamespaceCache) WaitForCacheSync(ctx context.Context) bool { diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/client.go index ad946daeaa..23d6103f63 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/client.go @@ -23,16 +23,21 @@ import ( "net/http" "strings" + "github.com/go-logr/logr" "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/runtime/serializer" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/metadata" "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/cache/cacheapi" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/client/internal/writebarrier" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -71,6 +76,9 @@ type Options struct { // - FieldValidationStrict // For more details, see: https://kubernetes.io/docs/reference/using-api/api-concepts/#field-validation FieldValidation string + + // Log will be used by the client if it encounters any errors. + Log logr.Logger } // CacheOptions are options for creating a cache-backed client. @@ -85,6 +93,23 @@ type CacheOptions struct { // read unstructured objects or lists from the cache. // If false, unstructured objects will always result in a live lookup. Unstructured bool + + // EnableReadYourWritesConsistency controls if read requests against the cache will + // block until the cache observed all write requests that started before the read + // request. Lists will wait for all pending write requests to the gvk the List is for. + // + // The `DisableReadYourWritesConsistency` option can be used to disable this functionality + // for individual requests, both on reads to keep them from waiting and on writes to + // prevent them from blocking subsequent reads. + // + // The blocking is always scoped to the representation (typed, unstructured or + // PartialObjectMetadata), meaning a write only blocks reads of the same representation. + // + // This is an experimental feature, a form of this will be kept but both the details of + // how exactly it works and how exactly it is configured may change. + // + // Defaults to false. + EnableReadYourWritesConsistency *bool } // NewClientFunc allows a user to define how to create a client. @@ -113,9 +138,17 @@ type NewClientFunc func(config *rest.Config, options Options) (Client, error) // corresponding group, version, and kind for the given type. In the // case of unstructured types, the group, version, and kind will be extracted // from the corresponding fields on the object. -func New(config *rest.Config, options Options) (c Client, err error) { - c, err = newClient(config, options) - if err == nil && options.DryRun != nil && *options.DryRun { +func New(config *rest.Config, options Options) (Client, error) { + _, c, err := newClient(config, options) + if err != nil { + return nil, err + } + + return wrapClient(c, options), nil +} + +func wrapClient(c Client, options Options) Client { + if options.DryRun != nil && *options.DryRun { c = NewDryRunClient(c) } if fo := options.FieldOwner; fo != "" { @@ -125,12 +158,12 @@ func New(config *rest.Config, options Options) (c Client, err error) { c = WithFieldValidation(c, FieldValidation(fv)) } - return c, err + return c } -func newClient(config *rest.Config, options Options) (*client, error) { +func newClient(config *rest.Config, options Options) (*client, Client, error) { if config == nil { - return nil, fmt.Errorf("must provide non-nil rest.Config to client.New") + return nil, nil, fmt.Errorf("must provide non-nil rest.Config to client.New") } config = rest.CopyConfig(config) @@ -152,7 +185,7 @@ func newClient(config *rest.Config, options Options) (*client, error) { var err error options.HTTPClient, err = rest.HTTPClientFor(config) if err != nil { - return nil, err + return nil, nil, err } } @@ -166,10 +199,14 @@ func newClient(config *rest.Config, options Options) (*client, error) { var err error options.Mapper, err = apiutil.NewDynamicRESTMapper(config, options.HTTPClient) if err != nil { - return nil, err + return nil, nil, err } } + if options.Log.IsZero() { + options.Log = log.Log.WithName("client") + } + resources := &clientRestResources{ httpClient: options.HTTPClient, config: config, @@ -182,7 +219,7 @@ func newClient(config *rest.Config, options Options) (*client, error) { rawMetaClient, err := metadata.NewForConfigAndClient(metadata.ConfigFor(config), options.HTTPClient) if err != nil { - return nil, fmt.Errorf("unable to construct metadata-only client for use as part of client: %w", err) + return nil, nil, fmt.Errorf("unable to construct metadata-only client for use as part of client: %w", err) } c := &client{ @@ -202,7 +239,7 @@ func newClient(config *rest.Config, options Options) (*client, error) { mapper: options.Mapper, } if options.Cache == nil || options.Cache.Reader == nil { - return c, nil + return c, c, nil } // We want a cache if we're here. @@ -215,11 +252,26 @@ func newClient(config *rest.Config, options Options) (*client, error) { for _, obj := range options.Cache.DisableFor { gvk, err := c.GroupVersionKindFor(obj) if err != nil { - return nil, err + return nil, nil, err } c.uncachedGVKs[gvk] = struct{}{} } - return c, nil + + if !ptr.Deref(options.Cache.EnableReadYourWritesConsistency, false) { + return c, c, nil + } + + informerCache, isCache := options.Cache.Reader.(cacheapi.Informers) + if !isCache { + return nil, nil, fmt.Errorf("cache reader does not implement %T, can not provide ReadYourWritesConsistency", cacheapi.Informers(nil)) + } + + return c, newConsistentClient( + c, + informerCache, + writebarrier.NewWriteBarrier, + options.Log, + ), nil } var _ Client = &client{} @@ -319,11 +371,18 @@ func (c *client) Update(ctx context.Context, obj Object, opts ...UpdateOption) e // Delete implements client.Client. func (c *client) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { + _, err := c.delete(ctx, obj, opts...) + return err +} + +// delete issues a delete call and returns the response or an error. The response +// gets deserialized into an unstructured and is either a metav1.Status if the object +// is gone from storage or the object if it remains, for example because of finalizers. +func (c *client) delete(ctx context.Context, obj Object, opts ...DeleteOption) (*unstructured.Unstructured, error) { switch obj.(type) { case runtime.Unstructured: return c.unstructuredClient.Delete(ctx, obj, opts...) - case *metav1.PartialObjectMetadata: - return c.metadataClient.Delete(ctx, obj, opts...) + // The typed client can also delete PartialObjectMeta default: return c.typedClient.Delete(ctx, obj, opts...) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/config/config.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/config/config.go index 1c39f4d854..9c7187046f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/config/config.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/config/config.go @@ -61,7 +61,7 @@ func RegisterFlags(fs *flag.FlagSet) { // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // -// The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and +// The returned `*rest.Config` has client-side rate limiting 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. // // Config precedence: @@ -81,7 +81,7 @@ func GetConfig() (*rest.Config, error) { // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // -// The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and +// The returned `*rest.Config` has client-side rate limiting 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. // // Config precedence: @@ -169,7 +169,7 @@ func loadConfigWithContext(apiServerURL string, loader clientcmd.ClientConfigLoa // If --kubeconfig is set, will use the kubeconfig file at that location. Otherwise will assume running // in cluster and use the cluster provided kubeconfig. // -// The returned `*rest.Config` has client-side ratelimting disabled as we can rely on API priority and +// The returned `*rest.Config` has client-side rate limiting 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. // // Will log an error and exit if there is an error creating the rest.Config. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/consistency.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/consistency.go new file mode 100644 index 0000000000..256f862055 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/consistency.go @@ -0,0 +1,494 @@ +/* +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 client + +import ( + "context" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + + "github.com/go-logr/logr" + "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/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/cache/cacheapi" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/client/internal/consistencyhandler" + "sigs.k8s.io/controller-runtime/pkg/client/internal/writebarrier" +) + +type consistentClientUpstream interface { + Client + + delete(ctx context.Context, obj Object, opts ...DeleteOption) (*unstructured.Unstructured, error) +} + +var _ Client = (*consistentClient)(nil) + +func newConsistentClient( + upstream consistentClientUpstream, + informers cacheapi.Informers, + newWriteBarrier func() writebarrier.WriteBarrier, + log logr.Logger, +) *consistentClient { + return &consistentClient{ + upstream: upstream, + informers: informers, + writeBarriers: newThreadSafeMap[gvkAndRepresentation](func() writebarrier.WriteBarriers { + return writebarrier.NewWriteBarriers(newWriteBarrier) + }), + consistencyHandlers: newThreadSafeMap[gvkAndRepresentation](func() *consistencyhandler.ConsistencyHandler { + return consistencyhandler.NewHandler(log) + }), + } +} + +type consistentClient struct { + upstream consistentClientUpstream + informers cacheapi.Informers + + writeBarriers *threadSafeMap[gvkAndRepresentation, writebarrier.WriteBarriers] + + consistencyHandlers *threadSafeMap[gvkAndRepresentation, *consistencyhandler.ConsistencyHandler] +} + +type gvkAndRepresentation struct { + gvk schema.GroupVersionKind + representation representationID +} + +func representationIDForObj(obj any) representationID { + switch obj.(type) { + case *unstructured.Unstructured, *unstructured.UnstructuredList: + return representationIDUnstructured + case *metav1.PartialObjectMetadata, *metav1.PartialObjectMetadataList: + return representationIDPartialObjectMetadata + default: + return representationIDTyped + } +} + +type representationID int8 + +const ( + representationIDUnstructured representationID = iota + representationIDPartialObjectMetadata + representationIDTyped +) + +func (c *consistentClient) getConsistencyHandler( + ctx context.Context, + gvkAndRepresentation gvkAndRepresentation, + obj Object, +) (*consistencyhandler.ConsistencyHandler, error) { + h := c.consistencyHandlers.getOrCreate(gvkAndRepresentation) + if h.Registered() { + return h, nil + } + + informer, err := c.informers.GetInformer(ctx, obj, cacheapi.BlockUntilSynced(true)) + if err != nil { + return nil, fmt.Errorf("failed to get informer for GVK %s: %w", gvkAndRepresentation.gvk, err) + } + + if err := h.Register(ctx, informer); err != nil { + return nil, fmt.Errorf("failed to register consistency handler on informer for GVK %s: %w", gvkAndRepresentation.gvk, err) + } + return h, nil +} + +func (c *consistentClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { + if (&GetOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency { + return c.upstream.Get(ctx, key, obj, opts...) + } + + gvk, err := apiutil.GVKForObject(obj, c.upstream.Scheme()) + if err != nil { + return fmt.Errorf("failed to get GVK for object %T: %w", obj, err) + } + gvkAndRepresentation := gvkAndRepresentation{gvk: gvk, representation: representationIDForObj(obj)} + + select { + case <-c.writeBarriers.getOrCreate(gvkAndRepresentation).Seal(key): + case <-ctx.Done(): + return ctx.Err() + } + + h, err := c.getConsistencyHandler(ctx, gvkAndRepresentation, obj) + if err != nil { + return err + } + if err := h.WaitForGet(ctx, key); err != nil { + return fmt.Errorf("failed to wait for cache to catch up: %w", err) + } + + return c.upstream.Get(ctx, key, obj, opts...) +} + +func (c *consistentClient) List(ctx context.Context, list ObjectList, opts ...ListOption) error { + if (&ListOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency { + return c.upstream.List(ctx, list, opts...) + } + + gvk, err := apiutil.GVKForObject(list, c.upstream.Scheme()) + if err != nil { + return fmt.Errorf("failed to get GVK for list %T: %w", list, err) + } + gvk.Kind = strings.TrimSuffix(gvk.Kind, "List") + gvkAndRepresentation := gvkAndRepresentation{gvk: gvk, representation: representationIDForObj(list)} + + for _, s := range c.writeBarriers.getOrCreate(gvkAndRepresentation).SealAll() { + select { + case <-s: + case <-ctx.Done(): + return ctx.Err() + } + } + + var obj Object + switch gvkAndRepresentation.representation { + case representationIDUnstructured: + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(gvk) + obj = u + case representationIDPartialObjectMetadata: + m := &metav1.PartialObjectMetadata{} + m.SetGroupVersionKind(gvk) + obj = m + case representationIDTyped: + raw, err := c.upstream.Scheme().New(gvk) + if err != nil { + return fmt.Errorf("failed to create object for GVK %s: %w", gvk, err) + } + asserted, ok := raw.(Object) + if !ok { + return fmt.Errorf("object of type %T for GVK %s does not implement Object", raw, gvk) + } + obj = asserted + } + + h, err := c.getConsistencyHandler(ctx, gvkAndRepresentation, obj) + if err != nil { + return err + } + if err := h.WaitForList(ctx); err != nil { + return fmt.Errorf("failed to wait for cache to catch up: %w", err) + } + + return c.upstream.List(ctx, list, opts...) +} + +func (c *consistentClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error { + return c.writeAndRecordRV(ctx, obj, (&CreateOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency, func() error { + return c.upstream.Create(ctx, obj, opts...) + }) +} + +func (c *consistentClient) Update(ctx context.Context, obj Object, opts ...UpdateOption) error { + return c.writeAndRecordRV(ctx, obj, (&UpdateOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency, func() error { + return c.upstream.Update(ctx, obj, opts...) + }) +} + +func (c *consistentClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error { + return c.writeAndRecordRV(ctx, obj, (&PatchOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency, func() error { + return c.upstream.Patch(ctx, obj, patch, opts...) + }) +} + +func (c *consistentClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error { + return c.writeAndRecordRV(ctx, obj, (&ApplyOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency, func() error { + return c.upstream.Apply(ctx, obj, opts...) + }) +} + +func writeTargetFor(obj any, scheme *runtime.Scheme) (gvkAndRepresentation, types.NamespacedName, Object, func() (string, error), error) { + switch t := obj.(type) { + case *unstructuredApplyConfiguration: + return gvkAndRepresentation{gvk: t.Unstructured.GroupVersionKind(), representation: representationIDUnstructured}, + ObjectKeyFromObject(t), + t.Unstructured, + func() (string, error) { return t.Unstructured.GetResourceVersion(), nil }, + nil + case applyConfiguration: + gvk, err := gvkFromApplyConfiguration(t) + if err != nil { + return gvkAndRepresentation{}, types.NamespacedName{}, nil, nil, fmt.Errorf("failed to get GVK for apply configuration %T: %w", obj, err) + } + cacheObj, err := scheme.New(gvk) + if err != nil { + return gvkAndRepresentation{}, types.NamespacedName{}, nil, nil, fmt.Errorf("failed to create object for GVK %s: %w", gvk, err) + } + clientObj, ok := cacheObj.(Object) + if !ok { + return gvkAndRepresentation{}, types.NamespacedName{}, nil, nil, fmt.Errorf("object of type %T for GVK %s does not implement client.Object", cacheObj, gvk) + } + clientObj.SetName(ptr.Deref(t.GetName(), "")) + clientObj.SetNamespace(ptr.Deref(t.GetNamespace(), "")) + return gvkAndRepresentation{gvk: gvk, representation: representationIDTyped}, + ObjectKeyFromObject(clientObj), + clientObj, + func() (string, error) { return resourceVersionFromApplyConfiguration(t) }, + nil + case *metav1.PartialObjectMetadata: + return gvkAndRepresentation{gvk: t.GroupVersionKind(), representation: representationIDPartialObjectMetadata}, + ObjectKeyFromObject(t), + t, + func() (string, error) { return t.GetResourceVersion(), nil }, + nil + case Object: + gvk, err := apiutil.GVKForObject(t, scheme) + if err != nil { + return gvkAndRepresentation{}, types.NamespacedName{}, nil, nil, fmt.Errorf("failed to get GVK for object %T: %w", obj, err) + } + return gvkAndRepresentation{gvk: gvk, representation: representationIDTyped}, + ObjectKeyFromObject(t), + t, + func() (string, error) { return t.GetResourceVersion(), nil }, + nil + default: + return gvkAndRepresentation{}, types.NamespacedName{}, nil, nil, fmt.Errorf("unsupported type %T, must be either %T, %T or %T", obj, Object(nil), &unstructuredApplyConfiguration{}, applyConfiguration(nil)) + } +} + +func (c *consistentClient) writeAndRecordRV(ctx context.Context, obj any, disableConsistency bool, write func() error) error { + if disableConsistency { + return write() + } + + gvkAndRepresentation, namespacedName, cacheObj, getResourceVersion, err := writeTargetFor(obj, c.upstream.Scheme()) + if err != nil { + return err + } + + // We don't technically need an informer since the RV is monotonically increasing, but we want to fail + // ASAP if the cache can not be setup. + h, err := c.getConsistencyHandler(ctx, gvkAndRepresentation, cacheObj) + if err != nil { + return err + } + + release := c.writeBarriers.getOrCreate(gvkAndRepresentation).Begin(namespacedName) + defer release() + + if err := write(); err != nil { + return err + } + + rvRaw, err := getResourceVersion() + if err != nil { + return fmt.Errorf("failed to get resource version from %T: %w", obj, err) + } + rv, err := strconv.ParseInt(rvRaw, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse resource version %s: %w", rvRaw, err) + } + + h.SetMinimumRV(ObjectKeyFromObject(cacheObj), rv) + + return nil +} + +func resourceVersionFromApplyConfiguration(obj applyConfiguration) (string, error) { + v := reflect.ValueOf(obj) + for v.Kind() == reflect.Pointer { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return "", fmt.Errorf("expected struct, got %s", v.Kind()) + } + rv := v.FieldByName("ResourceVersion") + if !rv.IsValid() { + return "", fmt.Errorf("type %T has no ResourceVersion field", obj) + } + if rv.Kind() != reflect.Pointer || rv.Type().Elem().Kind() != reflect.String { + return "", fmt.Errorf("ResourceVersion field in %T is not *string", obj) + } + if rv.IsNil() { + return "", fmt.Errorf("ResourceVersion field in %T is nil", obj) + } + return rv.Elem().String(), nil +} + +func (c *consistentClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { + if (&DeleteOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency { + return c.upstream.Delete(ctx, obj, opts...) + } + + gvk, err := apiutil.GVKForObject(obj, c.upstream.Scheme()) + if err != nil { + return fmt.Errorf("failed to get GVK for object %v: %w", obj, err) + } + + gvkAndRepresentation := gvkAndRepresentation{gvk: gvk, representation: representationIDForObj(obj)} + h, err := c.getConsistencyHandler(ctx, gvkAndRepresentation, obj) + if err != nil { + return err + } + + namespacedName := ObjectKeyFromObject(obj) + uid, err := c.uidForDelete(ctx, gvk, namespacedName, obj, opts...) + if err != nil { + return err + } + + release := c.writeBarriers.getOrCreate(gvkAndRepresentation).Begin(namespacedName) + defer release() + + // Register the delete before we execute it, otherwise it may be in the cache + // before we register it, causing a deadlock. + h.AddPendingDelete(namespacedName, uid) + + response, err := c.upstream.delete(ctx, obj, opts...) + if err != nil { + h.RemovePendingDelete(namespacedName, uid) + return err + } + + // Prefer waiting for the RV rather than an actual event since that is likely to be more resilient. + // This can only work if the response contains the RV which in turn only happens if the request did + // not delete the object from storage, for example because it had a finalizer. + if rvRaw := response.GetResourceVersion(); rvRaw != "" { + rv, err := strconv.ParseInt(rvRaw, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse resource version %s: %w", rvRaw, err) + } + h.RemovePendingDelete(namespacedName, uid) + h.SetMinimumRV(namespacedName, rv) + } + + return nil +} + +func (c *consistentClient) uidForDelete(ctx context.Context, gvk schema.GroupVersionKind, key ObjectKey, obj Object, opts ...DeleteOption) (types.UID, error) { + deleteOptions := (&DeleteOptions{}).ApplyOptions(opts) + if p := deleteOptions.Preconditions; p != nil && ptr.Deref(p.UID, "") != "" { + return *p.UID, nil + } + + if uid := obj.GetUID(); uid != "" { + return uid, nil + } + + existing, ok := obj.DeepCopyObject().(Object) + if !ok { + return "", fmt.Errorf("deepcopy of %T does not implement client.Object", obj) + } + if err := c.upstream.Get(ctx, key, existing); err != nil { + return "", fmt.Errorf("failed to get %s %s to determine its uid: %w", gvk.Kind, key, err) + } + + return existing.GetUID(), nil +} + +func (c *consistentClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error { + return errors.New("DeleteAllOf is not supported by consistentClient, please use List and Delete instead") +} + +func (c *consistentClient) Status() SubResourceWriter { + return c.SubResource("status") +} + +func (c *consistentClient) Scheme() *runtime.Scheme { + return c.upstream.Scheme() +} + +func (c *consistentClient) RESTMapper() meta.RESTMapper { + return c.upstream.RESTMapper() +} + +func (c *consistentClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { + return c.upstream.GroupVersionKindFor(obj) +} + +func (c *consistentClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { + return c.upstream.IsObjectNamespaced(obj) +} + +func (c *consistentClient) SubResource(subResource string) SubResourceClient { + return &consistentSubResourceClient{ + writeAndRecordRV: c.writeAndRecordRV, + upstream: c.upstream.SubResource(subResource), + } +} + +type consistentSubResourceClient struct { + writeAndRecordRV func(ctx context.Context, obj any, disableConsistency bool, write func() error) error + upstream SubResourceClient +} + +func (c *consistentSubResourceClient) Get(ctx context.Context, obj, subResource Object, opts ...SubResourceGetOption) error { + return c.upstream.Get(ctx, obj, subResource, opts...) +} + +func (c *consistentSubResourceClient) Create(ctx context.Context, obj, subResource Object, opts ...SubResourceCreateOption) error { + return c.writeAndRecordRV(ctx, obj, (&SubResourceCreateOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency, func() error { + return c.upstream.Create(ctx, obj, subResource, opts...) + }) +} + +func (c *consistentSubResourceClient) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error { + return c.writeAndRecordRV(ctx, obj, (&SubResourceUpdateOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency, func() error { + return c.upstream.Update(ctx, obj, opts...) + }) +} + +func (c *consistentSubResourceClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error { + return c.writeAndRecordRV(ctx, obj, (&SubResourcePatchOptions{}).ApplyOptions(opts).DisableReadYourWritesConsistency, func() error { + return c.upstream.Patch(ctx, obj, patch, opts...) + }) +} + +func (c *consistentSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...SubResourceApplyOption) error { + return c.writeAndRecordRV(ctx, obj, (&SubResourceApplyOptions{}).ApplyOpts(opts).DisableReadYourWritesConsistency, func() error { + return c.upstream.Apply(ctx, obj, opts...) + }) +} + +func newThreadSafeMap[k comparable, v any](newValue func() v) *threadSafeMap[k, v] { + return &threadSafeMap[k, v]{ + data: map[k]v{}, + newValue: newValue, + } +} + +type threadSafeMap[k comparable, v any] struct { + lock sync.Mutex + data map[k]v + newValue func() v +} + +func (t *threadSafeMap[k, v]) getOrCreate(key k) v { + t.lock.Lock() + defer t.lock.Unlock() + + val, exists := t.data[key] + if !exists { + val = t.newValue() + t.data[key] = val + } + + return val +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go index accf917cab..995d94780f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/client.go @@ -24,6 +24,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "time" /* @@ -130,6 +131,7 @@ type ClientBuilder struct { interceptorFuncs *interceptor.Funcs typeConverters []managedfields.TypeConverter returnManagedFields bool + globalRVCounter bool isBuilt bool // indexes maps each GroupVersionKind (GVK) to the indexes registered for that GVK. @@ -179,6 +181,15 @@ func (f *ClientBuilder) WithObjectTracker(ot testing.ObjectTracker) *ClientBuild return f } +// WithGlobalResourceVersionCounter makes the client use a global counter for resourceVersions +// rather than tracking them per object, mimicking what the apiserver does. +// +// Disabled by default. +func (f *ClientBuilder) WithGlobalResourceVersionCounter() *ClientBuilder { + f.globalRVCounter = true + return f +} + // WithIndex can be optionally used to register an index with name `field` and indexer `extractValue` // for API objects of the same GroupVersionKind (GVK) as `obj` in the fake client. // It can be invoked multiple times, both with objects of the same GVK or different ones. @@ -307,6 +318,9 @@ func (f *ClientBuilder) Build() client.WithWatch { withStatusSubresource: withStatusSubResource, usesFieldManagedObjectTracker: usesFieldManagedObjectTracker, } + if f.globalRVCounter { + tracker.resourceVersionCounter = &atomic.Uint64{} + } for _, obj := range f.initObject { if err := tracker.Add(obj); err != nil { @@ -343,14 +357,15 @@ func (f *ClientBuilder) Build() client.WithWatch { const trackerAddResourceVersion = "999" -// 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. +// convertFromUnstructuredOrPartialObjectMetaIfNecessary will convert runtime.Unstructured or metav1.PartialObjectMetadata +// 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 // tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure // we save as the very same type, otherwise subsequent List requests will fail. -func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) { - u, isUnstructured := o.(runtime.Unstructured) - if !isUnstructured { +func convertFromUnstructuredOrPartialObjectMetaIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) { + _, isUnstructured := o.(runtime.Unstructured) + _, isPartial := o.(*metav1.PartialObjectMetadata) + if !isUnstructured && !isPartial { return o, nil } gvk := o.GetObjectKind().GroupVersionKind() @@ -362,16 +377,17 @@ func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (ru if err != nil { return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", gvk, err) } - if _, isTypedUnstructured := typed.(runtime.Unstructured); isTypedUnstructured { + switch typed.(type) { + case runtime.Unstructured, *metav1.PartialObjectMetadata: return o, nil } - unstructuredSerialized, err := json.Marshal(u) + serialized, err := json.Marshal(o) if err != nil { - return nil, fmt.Errorf("failed to serialize %T: %w", unstructuredSerialized, err) + return nil, fmt.Errorf("failed to serialize %T: %w", o, err) } - if err := json.Unmarshal(unstructuredSerialized, typed); err != nil { - return nil, fmt.Errorf("failed to unmarshal the content of %T into %T: %w", u, typed, err) + if err := json.Unmarshal(serialized, typed); err != nil { + return nil, fmt.Errorf("failed to unmarshal the content of %T into %T: %w", o, typed, err) } return typed, nil @@ -812,12 +828,19 @@ func (c *fakeClient) update(obj client.Object, isStatus bool, opts ...client.Upd c.trackerWriteLock.Lock() defer c.trackerWriteLock.Unlock() - // Retain managed fields + // Retain the current managed fields when the incoming object leaves them + // unset (nil), mirroring apimachinery fieldmanager: a non-nil empty + // slice is treated as an explicit clear. Always retain on status + // updates, as managed fields can only be modified through the main + // resource: this is for example used by the csaupgrade package in + // client-go to migrate objects from client-side to server-side apply. // We can ignore all errors here since update will fail if we encounter an error. - obj.SetManagedFields(nil) - current, _ := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName()) - if currentMetaObj, ok := current.(metav1.Object); ok { - obj.SetManagedFields(currentMetaObj.GetManagedFields()) + if isStatus || obj.GetManagedFields() == nil { + obj.SetManagedFields(nil) + current, _ := c.tracker.Get(gvr, accessor.GetNamespace(), accessor.GetName()) + if currentMetaObj, ok := current.(metav1.Object); ok { + obj.SetManagedFields(currentMetaObj.GetManagedFields()) + } } if err := c.tracker.update(gvr, obj, accessor.GetNamespace(), isStatus, false, *updateOptions.AsUpdateOptions()); err != nil { @@ -858,7 +881,11 @@ func (c *fakeClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, return err } - acJSON, err := json.Marshal(u) + return copyIntoApplyConfiguration(u, obj) +} + +func copyIntoApplyConfiguration(from any, obj runtime.ApplyConfiguration) error { + acJSON, err := json.Marshal(from) if err != nil { return fmt.Errorf("failed to marshal patched object: %w", err) } @@ -942,7 +969,11 @@ func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client 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") + if c.tracker.resourceVersionCounter != nil { + obj.SetResourceVersion(c.tracker.nextResourceVersion()) + } else { + obj.SetResourceVersion("1") + } } else { // SSA deletionTimestamp updates are silently ignored obj.SetDeletionTimestamp(oldAccessor.GetDeletionTimestamp()) @@ -1331,10 +1362,6 @@ func (sw *fakeSubResourceClient) statusPatch(body client.Object, patch client.Pa } 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) @@ -1348,27 +1375,80 @@ func (sw *fakeSubResourceClient) Apply(ctx context.Context, obj runtime.ApplyCon return fmt.Errorf("failed to unmarshal apply configuration: %w", err) } - patchOpts := &client.SubResourcePatchOptions{} - patchOpts.Raw = applyOpts.AsPatchOptions() - - if applyOpts.SubResourceBody != nil { + switch sw.subResource { + case subResourceScale: + if applyOpts.SubResourceBody == nil { + return apierrors.NewBadRequest("missing SubResourceBody") + } 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 { + scale := &autoscalingv1.Scale{} + if err := json.Unmarshal(subResourceBodySerialized, scale); err != nil { return fmt.Errorf("failed to unmarshal subresource body: %w", err) } - patchOpts.SubResourceBody = subResourceBody - } - return sw.Patch(ctx, u, &fakeApplyPatch{}, patchOpts) + sw.client.schemeLock.RLock() + target, err := sw.client.scheme.New(u.GroupVersionKind()) + sw.client.schemeLock.RUnlock() + if err != nil { + return err + } + targetObj, isObject := target.(client.Object) + if !isObject { + return fmt.Errorf("%T is not a client.Object", target) + } + + if err := sw.client.Get(ctx, client.ObjectKeyFromObject(u), targetObj); err != nil { + return err + } + if err := applyScale(targetObj, scale); err != nil { + return err + } + if err := sw.client.update(targetObj, false, &client.UpdateOptions{FieldManager: applyOpts.FieldManager}); err != nil { + return err + } + + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(targetObj) + if err != nil { + return fmt.Errorf("failed to convert %T to unstructured: %w", targetObj, err) + } + result := &unstructured.Unstructured{Object: content} + result.SetGroupVersionKind(u.GroupVersionKind()) + + return copyIntoApplyConfiguration(result, obj) + case "status": + patchOpts := &client.SubResourcePatchOptions{} + patchOpts.Raw = applyOpts.AsPatchOptions() + + result := u + 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 + result = subResourceBody + } + + if err := sw.Patch(ctx, u, &fakeApplyPatch{}, patchOpts); err != nil { + return err + } + + return copyIntoApplyConfiguration(result, obj) + default: + return errors.New("fakeSubResourceClient currently only supports Apply for status and scale subresource") + } } func allowsUnconditionalUpdate(gvk schema.GroupVersionKind) bool { switch gvk.Group { - case "apps": + case "apps": //nolint:goconst switch gvk.Kind { case "ControllerRevision", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet": return true @@ -1526,6 +1606,24 @@ func getSingleOrZeroOptions[T any](opts []T) (opt T, err error) { func extractScale(obj client.Object) (*autoscalingv1.Scale, error) { switch obj := obj.(type) { + case *unstructured.Unstructured: + var typed client.Object + switch obj.GroupVersionKind() { + case appsv1.SchemeGroupVersion.WithKind("Deployment"): + typed = &appsv1.Deployment{} + case appsv1.SchemeGroupVersion.WithKind("ReplicaSet"): + typed = &appsv1.ReplicaSet{} + case appsv1.SchemeGroupVersion.WithKind("StatefulSet"): + typed = &appsv1.StatefulSet{} + case corev1.SchemeGroupVersion.WithKind("ReplicationController"): + typed = &corev1.ReplicationController{} + default: + return nil, fmt.Errorf("scale subresource for resource %T is not implemented", obj) + } + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, typed); err != nil { + return nil, err + } + return extractScale(typed) case *appsv1.Deployment: var replicas int32 = 1 if obj.Spec.Replicas != nil { @@ -1630,6 +1728,16 @@ func extractScale(obj client.Object) (*autoscalingv1.Scale, error) { func applyScale(obj client.Object, scale *autoscalingv1.Scale) error { switch obj := obj.(type) { + case *unstructured.Unstructured: + switch obj.GroupVersionKind() { + case appsv1.SchemeGroupVersion.WithKind("Deployment"), + appsv1.SchemeGroupVersion.WithKind("ReplicaSet"), + appsv1.SchemeGroupVersion.WithKind("StatefulSet"), + corev1.SchemeGroupVersion.WithKind("ReplicationController"): + return unstructured.SetNestedField(obj.Object, int64(scale.Spec.Replicas), "spec", "replicas") + default: + return fmt.Errorf("scale subresource for resource %T is not implemented", obj) + } case *appsv1.Deployment: obj.Spec.Replicas = new(scale.Spec.Replicas) case *appsv1.ReplicaSet: @@ -1645,12 +1753,13 @@ func applyScale(obj client.Object, scale *autoscalingv1.Scale) error { return nil } -// AddIndex adds an index to a fake client. It will panic if used with a client that is not a fake client. +// AddIndex adds an index to a fake client. It will panic if used with a client that is not a fake client, +// or a wrapper around one, such as the client returned by ClientBuilder.WithInterceptorFuncs. // It will error if there is already an index for given object with the same name as field. // // It can be used to test code that adds indexes to the cache at runtime. func AddIndex(c client.Client, obj runtime.Object, field string, extractValue client.IndexerFunc) error { - fakeClient, isFakeClient := c.(*fakeClient) + fakeClient, isFakeClient := unwrapFakeClient(c) if !isFakeClient { panic("AddIndex can only be used with a fake client") } @@ -1679,6 +1788,21 @@ func AddIndex(c client.Client, obj runtime.Object, field string, extractValue cl return nil } +// unwrapFakeClient returns the *fakeClient backing c, unwrapping any clients that wrap it, such as the +// interceptor client returned by ClientBuilder.WithInterceptorFuncs. +func unwrapFakeClient(c client.Client) (*fakeClient, bool) { + for { + if fc, ok := c.(*fakeClient); ok { + return fc, true + } + unwrapper, ok := c.(interface{ Unwrap() client.WithWatch }) + if !ok { + return nil, false + } + c = unwrapper.Unwrap() + } +} + func (c *fakeClient) addToSchemeIfUnknownAndUnstructuredOrPartial(obj runtime.Object) error { c.schemeLock.Lock() defer c.schemeLock.Unlock() diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/versioned_tracker.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/versioned_tracker.go index 62ccac2bec..ef615cf23e 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/versioned_tracker.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/fake/versioned_tracker.go @@ -22,6 +22,7 @@ import ( "fmt" "runtime/debug" "strconv" + "sync/atomic" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -44,6 +45,12 @@ type versionedTracker struct { scheme *runtime.Scheme withStatusSubresource sets.Set[schema.GroupVersionKind] usesFieldManagedObjectTracker bool + + resourceVersionCounter *atomic.Uint64 +} + +func (t versionedTracker) nextResourceVersion() string { + return strconv.FormatUint(t.resourceVersionCounter.Add(1), 10) } func (t versionedTracker) Add(obj runtime.Object) error { @@ -66,14 +73,18 @@ func (t versionedTracker) Add(obj runtime.Object) error { 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) + if t.resourceVersionCounter != nil { + accessor.SetResourceVersion(t.nextResourceVersion()) + } else { + // 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) + obj, err = convertFromUnstructuredOrPartialObjectMetaIfNecessary(t.scheme, obj) if err != nil { return err } @@ -87,6 +98,18 @@ func (t versionedTracker) Add(obj runtime.Object) error { return fmt.Errorf("invalid managedFields on %T: %w", obj, err) } } + // The trackers add panics when using PartialObjectMetadata without registering it to + // the scheme. + if _, isPartial := obj.(*metav1.PartialObjectMetadata); isPartial { + gvk, err := apiutil.GVKForObject(obj, t.scheme) + if err != nil { + return fmt.Errorf("failed to get gvk for %T: %w", obj, err) + } + if !t.scheme.Recognizes(gvk) { + t.scheme.AddKnownTypeWithName(gvk, obj) + } + } + if err := t.upstream.Add(obj); err != nil { return err } @@ -110,8 +133,12 @@ func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Ob if accessor.GetResourceVersion() != "" { return apierrors.NewBadRequest("resourceVersion can not be set for Create requests") } - accessor.SetResourceVersion("1") - obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) + if t.resourceVersionCounter != nil { + accessor.SetResourceVersion(t.nextResourceVersion()) + } else { + accessor.SetResourceVersion("1") + } + obj, err = convertFromUnstructuredOrPartialObjectMetaIfNecessary(t.scheme, obj) if err != nil { return err } @@ -286,12 +313,16 @@ func (t versionedTracker) updateObject( 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) + if t.resourceVersionCounter != nil { + accessor.SetResourceVersion(t.nextResourceVersion()) + } else { + 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)) } - 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()) @@ -301,7 +332,7 @@ func (t versionedTracker) updateObject( return nil, false, t.Delete(gvr, accessor.GetNamespace(), accessor.GetName(), metav1.DeleteOptions{DryRun: dryRun}) } - obj, err = convertFromUnstructuredIfNecessary(t.scheme, obj) + obj, err = convertFromUnstructuredOrPartialObjectMetaIfNecessary(t.scheme, obj) return obj, false, err } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/interceptor/intercept.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/interceptor/intercept.go index b98af1a693..5b9751edc0 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/interceptor/intercept.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/interceptor/intercept.go @@ -1,3 +1,19 @@ +/* +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 interceptor import ( @@ -44,6 +60,13 @@ type interceptor struct { var _ client.WithWatch = &interceptor{} +// Unwrap returns the client that is wrapped by this interceptor. This is used by consumers that need to +// access functionality of the wrapped client that is not part of the client.WithWatch interface, such as +// sigs.k8s.io/controller-runtime/pkg/client/fake.AddIndex. +func (c interceptor) Unwrap() client.WithWatch { + return c.client +} + func (c interceptor) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { return c.client.GroupVersionKindFor(obj) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/interfaces.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/interfaces.go index 1af1f3a368..87c6d17ae9 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/interfaces.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/interfaces.go @@ -26,6 +26,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" + + "sigs.k8s.io/controller-runtime/pkg/cache/cacheapi" ) // ObjectKey identifies a Kubernetes Object. @@ -194,20 +196,11 @@ type WithWatch interface { // IndexerFunc knows how to take an object and turn it into a series // of non-namespaced keys. Namespaced objects are automatically given // namespaced and non-spaced variants, so keys do not need to include namespace. -type IndexerFunc func(Object) []string +type IndexerFunc = cacheapi.IndexerFunc // FieldIndexer knows how to index over a particular "field" such that it // can later be used by a field selector. -type FieldIndexer interface { - // IndexField adds an index with the given field name on the given object type - // by using the given function to extract the value for that field. If you want - // compatibility with the Kubernetes API server, only return one key, and only use - // fields that the API server supports. Otherwise, you can return multiple keys, - // and "equality" in the field selector means that at least one key matches the value. - // The FieldIndexer will automatically take care of indexing over namespace - // and supporting efficient all-namespace queries. - IndexField(ctx context.Context, obj Object, field string, extractValue IndexerFunc) error -} +type FieldIndexer = cacheapi.FieldIndexer // IgnoreNotFound returns nil on NotFound errors. // All other values that are not NotFound errors or nil are returned unmodified. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/internal/consistencyhandler/consistencyhandler.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/internal/consistencyhandler/consistencyhandler.go new file mode 100644 index 0000000000..9c247c2df5 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/internal/consistencyhandler/consistencyhandler.go @@ -0,0 +1,345 @@ +/* +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 consistencyhandler + +import ( + "context" + "fmt" + "maps" + "strconv" + "sync" + "sync/atomic" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/tools/cache" + "sigs.k8s.io/controller-runtime/pkg/cache/cacheapi" +) + +func NewHandler(log logr.Logger) *ConsistencyHandler { + return &ConsistencyHandler{ + rvBroadCaster: &broadcaster{}, + pendingDeletesBroadcaster: &broadcaster{}, + pendingDeletes: make(map[types.NamespacedName]sets.Set[types.UID]), + minimumRVs: make(map[types.NamespacedName]int64), + log: log, + triggerCleanup: make(chan struct{}, 1), + } +} + +type ConsistencyHandler struct { + rvBroadCaster *broadcaster + observedRV atomic.Int64 + + pendingDeletesBroadcaster *broadcaster + pendingDeletesLock sync.RWMutex + // pendingDeletes holds pending deletes. Must only be accessed when holding pendingDeletesLock + pendingDeletes map[types.NamespacedName]sets.Set[types.UID] + + minimumRVsLock sync.RWMutex + // minimumRVs stores the minimum RVs we must have seen before returning reads. Must only be + // accessed when holding minimumRVsLock. + minimumRVs map[types.NamespacedName]int64 + + registerLock sync.RWMutex + registered bool + registration *cache.ResourceEventHandlerRegistration + + log logr.Logger + + // triggerCleanup triggers the cleanup of pendingRVs that are smaller + // than observedRV. It holds a one element buffered channel, because + // having more than one queued cleanup doesn't make sense. + triggerCleanup chan struct{} +} + +func (h *ConsistencyHandler) Registered() bool { + h.registerLock.RLock() + defer h.registerLock.RUnlock() + return h.registered +} + +func (h *ConsistencyHandler) Register(ctx context.Context, informer cacheapi.Informer) error { + h.registerLock.Lock() + defer h.registerLock.Unlock() + + // Check again in case it got registered while we waited for the lock + if h.registered { + return nil + } + + if h.registration != nil { + return h.waitForHandlerSyncLocked(ctx, *h.registration) + } + + registration, err := informer.AddEventHandler(h) + if err != nil { + return fmt.Errorf("failed to add consistency handler to informer: %w", err) + } + h.registration = ®istration + + return h.waitForHandlerSyncLocked(ctx, registration) +} + +func (h *ConsistencyHandler) waitForHandlerSyncLocked(ctx context.Context, registration cache.ResourceEventHandlerRegistration) error { + select { + case <-registration.HasSyncedChecker().Done(): + h.registered = true + return nil + case <-ctx.Done(): + return fmt.Errorf("failed waiting for consistency handler to sync: %w", ctx.Err()) + } +} + +func (h *ConsistencyHandler) SetMinimumRV(key types.NamespacedName, rv int64) { + h.minimumRVsLock.Lock() + defer h.minimumRVsLock.Unlock() + if rv > h.minimumRVs[key] && rv > h.observedRV.Load() { + h.minimumRVs[key] = rv + } +} + +func (h *ConsistencyHandler) getMinimumRVForKey(key types.NamespacedName) int64 { + h.minimumRVsLock.RLock() + defer h.minimumRVsLock.RUnlock() + return h.minimumRVs[key] +} + +func (h *ConsistencyHandler) getMinimumRVForGVK() int64 { + h.minimumRVsLock.RLock() + defer h.minimumRVsLock.RUnlock() + var maxRV int64 + for _, rv := range h.minimumRVs { + if rv > maxRV { + maxRV = rv + } + } + return maxRV +} + +func (h *ConsistencyHandler) cleanupMinimumRVs() { + h.minimumRVsLock.Lock() + defer h.minimumRVsLock.Unlock() + for key, rv := range h.minimumRVs { + if rv <= h.observedRV.Load() { + delete(h.minimumRVs, key) + } + } +} + +func (h *ConsistencyHandler) AddPendingDelete(key types.NamespacedName, uid types.UID) { + h.pendingDeletesLock.Lock() + defer h.pendingDeletesLock.Unlock() + + if h.pendingDeletes[key] == nil { + h.pendingDeletes[key] = sets.New(uid) + return + } + h.pendingDeletes[key].Insert(uid) +} + +func (h *ConsistencyHandler) RemovePendingDelete(key types.NamespacedName, uid types.UID) { + h.pendingDeletesLock.Lock() + defer h.pendingDeletesLock.Unlock() + + if h.pendingDeletes[key] != nil { + h.pendingDeletes[key].Delete(uid) + if len(h.pendingDeletes[key]) == 0 { + delete(h.pendingDeletes, key) + } + h.pendingDeletesBroadcaster.broadcast() + } +} + +func (h *ConsistencyHandler) WaitForList(ctx context.Context) error { + // snapshot deletes before waiting for the RV so we don't end up including + // deletes that came in while we waited. + h.pendingDeletesLock.RLock() + pendingDeletes := sets.Set[types.UID]{} + for _, uids := range h.pendingDeletes { + maps.Copy(pendingDeletes, uids) + } + h.pendingDeletesLock.RUnlock() + + if err := h.waitForRV(ctx, h.getMinimumRVForGVK()); err != nil { + return err + } + + return h.waitDeletes(ctx, pendingDeletes) +} + +func (h *ConsistencyHandler) WaitForGet(ctx context.Context, key types.NamespacedName) error { + // snapshot deletes before waiting for the RV so we don't end up including + // deletes that came in while we waited. + h.pendingDeletesLock.RLock() + pendingDeletes := maps.Clone(h.pendingDeletes[key]) + h.pendingDeletesLock.RUnlock() + + if err := h.waitForRV(ctx, h.getMinimumRVForKey(key)); err != nil { + return err + } + + return h.waitDeletes(ctx, pendingDeletes) +} + +func (h *ConsistencyHandler) waitDeletes(ctx context.Context, uids sets.Set[types.UID]) error { + if len(uids) == 0 { + return nil + } + + for { + // must store the chan before checking the deletes to guarantee that even if the deletes + // get updated after our check and before the select, we still get an event. + updatedChan := h.pendingDeletesBroadcaster.getWaitChan() + done := h.allDeleted(uids) + if done { + return nil + } + + select { + case <-updatedChan: + continue + case <-ctx.Done(): + return fmt.Errorf("failed waiting to observe deletes for uids %v: %w", sets.List(uids), ctx.Err()) + } + } +} + +func (h *ConsistencyHandler) allDeleted(uids sets.Set[types.UID]) bool { + h.pendingDeletesLock.RLock() + defer h.pendingDeletesLock.RUnlock() + + for wantDeleted := range uids { + for _, notDeletedUIDs := range h.pendingDeletes { + if notDeletedUIDs.Has(wantDeleted) { + return false + } + } + } + + return true +} + +func (h *ConsistencyHandler) observeDeletion(obj cacheapi.Object) { + h.RemovePendingDelete(types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, obj.GetUID()) +} + +func (h *ConsistencyHandler) waitForRV(ctx context.Context, rv int64) error { + for { + // must store the chan before checking the RV to guarantee that even if the RV + // gets updated after our check and before the select, we still get an event. + updatedChan := h.rvBroadCaster.getWaitChan() + if h.observedRV.Load() >= rv { + return nil + } + select { + case <-updatedChan: + continue + case <-ctx.Done(): + return fmt.Errorf("failed waiting to observe resource version %d: %w", rv, ctx.Err()) + } + } +} + +func (h *ConsistencyHandler) observeResourceVersion(rv string) { + parsed, err := strconv.ParseInt(rv, 10, 64) + if err != nil { + h.log.Error(err, "Failed to parse resource version as int64", "resourceVersion", rv) + return + } + + for { + current := h.observedRV.Load() + if parsed <= current { + return + } + + if h.observedRV.CompareAndSwap(current, parsed) { + break + } + } + + h.rvBroadCaster.broadcast() + + select { + case h.triggerCleanup <- struct{}{}: + h.cleanupMinimumRVs() + <-h.triggerCleanup + default: + } +} + +func (h *ConsistencyHandler) OnAdd(raw any, _ bool) { + obj, ok := raw.(cacheapi.Object) + if !ok { + h.log.Error(nil, "OnAdd received object that is not a cacheapi.Object", "object", raw) + return + } + go func() { h.observeResourceVersion(obj.GetResourceVersion()) }() +} + +func (h *ConsistencyHandler) OnUpdate(_, newObj any) { + obj, ok := newObj.(cacheapi.Object) + if !ok { + h.log.Error(nil, "OnUpdate received object that is not a cacheapi.Object", "object", newObj) + return + } + go func() { h.observeResourceVersion(obj.GetResourceVersion()) }() +} + +func (h *ConsistencyHandler) OnDelete(raw any) { + var obj cacheapi.Object + switch t := raw.(type) { + case cacheapi.Object: + obj = t + case cache.DeletedFinalStateUnknown: + obj = t.Obj.(cacheapi.Object) + default: + h.log.Error(nil, "OnDelete received object that is not a cacheapi.Object or toolscache.DeletedFinalStateUnknown", "object", raw) + return + } + go func() { h.observeResourceVersion(obj.GetResourceVersion()) }() + go func() { h.observeDeletion(obj) }() +} + +// broadcaster wakes arbitrarily many waiters on broadcast(). +type broadcaster struct { + lock sync.Mutex + ch chan struct{} +} + +func (b *broadcaster) broadcast() { + b.lock.Lock() + defer b.lock.Unlock() + // Lazily create the channel in getWaitChan to avoid creating a channel per event. + if b.ch != nil { + close(b.ch) + b.ch = nil + } +} + +func (b *broadcaster) getWaitChan() <-chan struct{} { + b.lock.Lock() + defer b.lock.Unlock() + + if b.ch == nil { + b.ch = make(chan struct{}) + } + + return b.ch +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/internal/writebarrier/writebarrier.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/internal/writebarrier/writebarrier.go new file mode 100644 index 0000000000..77b5f3cbe3 --- /dev/null +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/internal/writebarrier/writebarrier.go @@ -0,0 +1,195 @@ +/* +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 writebarrier + +import ( + "sync" + + "k8s.io/apimachinery/pkg/types" +) + +// WriteBarriers adds garbage collection for unused keys to WriteBarrier. +type WriteBarriers interface { + // Begin keeps the passed key locked until the returned release func was called. + Begin(key types.NamespacedName) (release func()) + + // Seal seals the currently-active set of locks to key and returns a channel that + // closes when they are done. + Seal(key types.NamespacedName) <-chan struct{} + + // SealAll is Seal for all keys. + SealAll() []<-chan struct{} +} + +// NewWriteBarriers construct WriteBarriers. newBarrier is configurable for testing purposes only. +func NewWriteBarriers(newBarrier func() WriteBarrier) WriteBarriers { + return &writeBarriers{ + data: map[types.NamespacedName]*writeBarrierWithRefCounter{}, + newBarrier: newBarrier, + } +} + +type writeBarrierWithRefCounter struct { + WriteBarrier + inFlightWrites int +} + +// writeBarriers holds one writeBarrier per key that has an in-flight write. +type writeBarriers struct { + lock sync.Mutex + data map[types.NamespacedName]*writeBarrierWithRefCounter + newBarrier func() WriteBarrier +} + +func (w *writeBarriers) Begin(key types.NamespacedName) func() { + w.lock.Lock() + defer w.lock.Unlock() + + barrier, exists := w.data[key] + if !exists { + barrier = &writeBarrierWithRefCounter{WriteBarrier: w.newBarrier()} + w.data[key] = barrier + } + barrier.inFlightWrites++ + release := barrier.Begin() + + return func() { + release() + + w.lock.Lock() + defer w.lock.Unlock() + barrier.inFlightWrites-- + if barrier.inFlightWrites == 0 { + delete(w.data, key) + } + } +} + +func (w *writeBarriers) Seal(key types.NamespacedName) <-chan struct{} { + w.lock.Lock() + defer w.lock.Unlock() + + barrier, exists := w.data[key] + if !exists { + return closedChannel + } + + return barrier.Seal() +} + +func (w *writeBarriers) SealAll() []<-chan struct{} { + w.lock.Lock() + defer w.lock.Unlock() + + result := make([]<-chan struct{}, 0, len(w.data)) + for _, barrier := range w.data { + result = append(result, barrier.Seal()) + } + + return result +} + +var closedChannel chan struct{} + +func init() { + closedChannel = make(chan struct{}) + close(closedChannel) +} + +type WriteBarrier interface { + Begin() (release func()) + Seal() <-chan struct{} +} + +// NewWriteBarrier creates a new WriteBarrier +func NewWriteBarrier() WriteBarrier { + return &keyWriteBarrier{previous: closedChannel} +} + +// keyWriteBarrier allows to wait for a set of in-flight writes to finish. +type keyWriteBarrier struct { + // lock must be held to access current or previous. + lock sync.Mutex + + // current is the current write batch. It has a reference to the key write + // barrier that it uses to delete itself once done. + current *writeBatch + + // previous is closed once all previous write batches are done. + previous <-chan struct{} +} + +// Begin adds a write to the current batch, starting one if needed. +func (b *keyWriteBarrier) Begin() func() { + b.lock.Lock() + defer b.lock.Unlock() + + if b.current == nil { + b.current = &writeBatch{barrier: b, done: make(chan struct{})} + } + b.current.inFlight++ + + return b.current.release +} + +func (b *keyWriteBarrier) Seal() <-chan struct{} { + b.lock.Lock() + defer b.lock.Unlock() + + if b.current == nil { + return b.previous + } + + // Create a new chan that blocks until both previous + // and current are done. + done := make(chan struct{}) + previous := b.previous + b.previous = done + + current := b.current.done + b.current = nil + + go func() { + for _, c := range []<-chan struct{}{previous, current} { + <-c + } + close(done) + }() + + return done +} + +type writeBatch struct { + barrier *keyWriteBarrier + inFlight int + done chan struct{} +} + +func (w *writeBatch) release() { + w.barrier.lock.Lock() + defer w.barrier.lock.Unlock() + + w.inFlight-- + if w.inFlight > 0 { + return + } + + close(w.done) + if w.barrier.current == w { + w.barrier.current = nil + } +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/metadata_client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/metadata_client.go index d0c6b8e13a..6c0a2078d0 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/metadata_client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/metadata_client.go @@ -49,24 +49,6 @@ func (mc *metadataClient) getResourceInterface(gvk schema.GroupVersionKind, ns s return mc.client.Resource(mapping.Resource).Namespace(ns), nil } -// Delete implements client.Client. -func (mc *metadataClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { - metadata, ok := obj.(*metav1.PartialObjectMetadata) - if !ok { - return fmt.Errorf("metadata client did not understand object: %T", obj) - } - - resInt, err := mc.getResourceInterface(metadata.GroupVersionKind(), metadata.Namespace) - if err != nil { - return err - } - - deleteOpts := DeleteOptions{} - deleteOpts.ApplyOptions(opts) - - return resInt.Delete(ctx, metadata.Name, *deleteOpts.AsDeleteOptions()) -} - // DeleteAllOf implements client.Client. func (mc *metadataClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error { metadata, ok := obj.(*metav1.PartialObjectMetadata) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/object.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/object.go index 31e334d6c2..e5f820438d 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/object.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/object.go @@ -19,6 +19,8 @@ package client import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + + "sigs.k8s.io/controller-runtime/pkg/cache/cacheapi" ) // Object is a Kubernetes object, allows functions to work indistinctly with @@ -42,10 +44,7 @@ import ( // a `metadata` field). // // Notice that XYZList types are distinct: they implement ObjectList instead. -type Object interface { - metav1.Object - runtime.Object -} +type Object = cacheapi.Object // ObjectList is a Kubernetes object list, allows functions to work // indistinctly with any resource that implements both runtime.Object and diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/options.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/options.go index c5bd9f0b8c..aae55edfcb 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/options.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/options.go @@ -200,6 +200,72 @@ func (f FieldOwner) ApplyToSubResourceApply(opts *SubResourceApplyOptions) { opts.FieldManager = string(f) } +// DisableReadYourWritesConsistency disables read-your-own-write consistency for the +// request it is passed to. It has no effect if the client was not configured with +// CacheOptions.EnableReadYourWritesConsistency. +// +// On a read request, the request will not wait for the cache to have observed +// previous writes. On a write request, subsequent read requests will not wait +// for the cache to have observed this write. +const DisableReadYourWritesConsistency = disableReadYourWritesConsistency(true) + +type disableReadYourWritesConsistency bool + +// ApplyToGet applies this configuration to the given get options. +func (disableReadYourWritesConsistency) ApplyToGet(opts *GetOptions) { + opts.DisableReadYourWritesConsistency = true +} + +// ApplyToList applies this configuration to the given list options. +func (disableReadYourWritesConsistency) ApplyToList(opts *ListOptions) { + opts.DisableReadYourWritesConsistency = true +} + +// ApplyToCreate applies this configuration to the given create options. +func (disableReadYourWritesConsistency) ApplyToCreate(opts *CreateOptions) { + opts.DisableReadYourWritesConsistency = true +} + +// ApplyToUpdate applies this configuration to the given update options. +func (disableReadYourWritesConsistency) ApplyToUpdate(opts *UpdateOptions) { + opts.DisableReadYourWritesConsistency = true +} + +// ApplyToPatch applies this configuration to the given patch options. +func (disableReadYourWritesConsistency) ApplyToPatch(opts *PatchOptions) { + opts.DisableReadYourWritesConsistency = true +} + +// ApplyToApply applies this configuration to the given apply options. +func (disableReadYourWritesConsistency) ApplyToApply(opts *ApplyOptions) { + opts.DisableReadYourWritesConsistency = true +} + +// ApplyToDelete applies this configuration to the given delete options. +func (disableReadYourWritesConsistency) ApplyToDelete(opts *DeleteOptions) { + opts.DisableReadYourWritesConsistency = true +} + +// ApplyToSubResourceCreate applies this configuration to the given create options. +func (d disableReadYourWritesConsistency) ApplyToSubResourceCreate(opts *SubResourceCreateOptions) { + d.ApplyToCreate(&opts.CreateOptions) +} + +// ApplyToSubResourceUpdate applies this configuration to the given update options. +func (d disableReadYourWritesConsistency) ApplyToSubResourceUpdate(opts *SubResourceUpdateOptions) { + d.ApplyToUpdate(&opts.UpdateOptions) +} + +// ApplyToSubResourcePatch applies this configuration to the given patch options. +func (d disableReadYourWritesConsistency) ApplyToSubResourcePatch(opts *SubResourcePatchOptions) { + d.ApplyToPatch(&opts.PatchOptions) +} + +// ApplyToSubResourceApply applies this configuration to the given apply options. +func (d disableReadYourWritesConsistency) ApplyToSubResourceApply(opts *SubResourceApplyOptions) { + d.ApplyToApply(&opts.ApplyOptions) +} + // FieldValidation configures field validation for the given requests. type FieldValidation string @@ -269,6 +335,11 @@ type CreateOptions struct { // will contain all unknown and duplicate fields encountered. FieldValidation string + // DisableReadYourWritesConsistency indicates that subsequent read requests will not + // wait for the cache to have observed this write. It has no effect if the client + // was not configured with CacheOptions.EnableReadYourWritesConsistency. + DisableReadYourWritesConsistency bool + // Raw represents raw CreateOptions, as passed to the API server. Raw *metav1.CreateOptions } @@ -309,6 +380,9 @@ func (o *CreateOptions) ApplyToCreate(co *CreateOptions) { if o.FieldValidation != "" { co.FieldValidation = o.FieldValidation } + if o.DisableReadYourWritesConsistency { + co.DisableReadYourWritesConsistency = true + } if o.Raw != nil { co.Raw = o.Raw } @@ -343,6 +417,11 @@ type DeleteOptions struct { // foreground. PropagationPolicy *metav1.DeletionPropagation + // DisableReadYourWritesConsistency indicates that subsequent read requests will not + // wait for the cache to have observed this delete. It has no effect if the client + // was not configured with CacheOptions.EnableReadYourWritesConsistency. + DisableReadYourWritesConsistency bool + // Raw represents raw DeleteOptions, as passed to the API server. Raw *metav1.DeleteOptions @@ -393,6 +472,9 @@ func (o *DeleteOptions) ApplyToDelete(do *DeleteOptions) { if o.PropagationPolicy != nil { do.PropagationPolicy = o.PropagationPolicy } + if o.DisableReadYourWritesConsistency { + do.DisableReadYourWritesConsistency = true + } if o.Raw != nil { do.Raw = o.Raw } @@ -468,6 +550,11 @@ type GetOptions struct { // otherwise you will mutate the object in the cache. // +optional UnsafeDisableDeepCopy *bool + + // DisableReadYourWritesConsistency indicates that the request will not wait for the + // cache to have observed previous writes. It has no effect if the client was not + // configured with CacheOptions.EnableReadYourWritesConsistency. + DisableReadYourWritesConsistency bool } var _ GetOption = &GetOptions{} @@ -480,6 +567,9 @@ func (o *GetOptions) ApplyToGet(lo *GetOptions) { if o.UnsafeDisableDeepCopy != nil { lo.UnsafeDisableDeepCopy = o.UnsafeDisableDeepCopy } + if o.DisableReadYourWritesConsistency { + lo.DisableReadYourWritesConsistency = true + } } // AsGetOptions returns these options as a flattened metav1.GetOptions. @@ -538,6 +628,11 @@ type ListOptions struct { // +optional UnsafeDisableDeepCopy *bool + // DisableReadYourWritesConsistency indicates that the request will not wait for the + // cache to have observed previous writes. It has no effect if the client was not + // configured with CacheOptions.EnableReadYourWritesConsistency. + DisableReadYourWritesConsistency bool + // Raw represents raw ListOptions, as passed to the API server. Note // that these may not be respected by all implementations of interface, // and the LabelSelector, FieldSelector, Limit and Continue fields are ignored. @@ -569,6 +664,10 @@ func (o *ListOptions) ApplyToList(lo *ListOptions) { if o.UnsafeDisableDeepCopy != nil { lo.UnsafeDisableDeepCopy = o.UnsafeDisableDeepCopy } + + if o.DisableReadYourWritesConsistency { + lo.DisableReadYourWritesConsistency = true + } } // AsListOptions returns these options as a flattened metav1.ListOptions. @@ -797,6 +896,11 @@ type UpdateOptions struct { // will contain all unknown and duplicate fields encountered. FieldValidation string + // DisableReadYourWritesConsistency indicates that subsequent read requests will not + // wait for the cache to have observed this write. It has no effect if the client + // was not configured with CacheOptions.EnableReadYourWritesConsistency. + DisableReadYourWritesConsistency bool + // Raw represents raw UpdateOptions, as passed to the API server. Raw *metav1.UpdateOptions } @@ -839,6 +943,9 @@ func (o *UpdateOptions) ApplyToUpdate(uo *UpdateOptions) { if o.FieldValidation != "" { uo.FieldValidation = o.FieldValidation } + if o.DisableReadYourWritesConsistency { + uo.DisableReadYourWritesConsistency = true + } if o.Raw != nil { uo.Raw = o.Raw } @@ -885,6 +992,11 @@ type PatchOptions struct { // will contain all unknown and duplicate fields encountered. FieldValidation string + // DisableReadYourWritesConsistency indicates that subsequent read requests will not + // wait for the cache to have observed this write. It has no effect if the client + // was not configured with CacheOptions.EnableReadYourWritesConsistency. + DisableReadYourWritesConsistency bool + // Raw represents raw PatchOptions, as passed to the API server. Raw *metav1.PatchOptions } @@ -939,6 +1051,9 @@ func (o *PatchOptions) ApplyToPatch(po *PatchOptions) { if o.FieldValidation != "" { po.FieldValidation = o.FieldValidation } + if o.DisableReadYourWritesConsistency { + po.DisableReadYourWritesConsistency = true + } if o.Raw != nil { po.Raw = o.Raw } @@ -1020,6 +1135,11 @@ type ApplyOptions struct { // // +required FieldManager string + + // DisableReadYourWritesConsistency indicates that subsequent read requests will not + // wait for the cache to have observed this write. It has no effect if the client + // was not configured with CacheOptions.EnableReadYourWritesConsistency. + DisableReadYourWritesConsistency bool } // ApplyOptions applies the given opts onto the ApplyOptions @@ -1042,6 +1162,10 @@ func (o *ApplyOptions) ApplyToApply(opts *ApplyOptions) { if o.FieldManager != "" { opts.FieldManager = o.FieldManager } + + if o.DisableReadYourWritesConsistency { + opts.DisableReadYourWritesConsistency = true + } } // AsPatchOptions constructs patch options from the given ApplyOptions diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/typed_client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/typed_client.go index 5a85941725..396bc6ea63 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/typed_client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/typed_client.go @@ -18,15 +18,15 @@ package client import ( "context" + "encoding/json" "fmt" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/json" "k8s.io/client-go/util/apply" ) var _ Reader = &typedClient{} -var _ Writer = &typedClient{} type typedClient struct { resources *clientRestResources @@ -73,22 +73,35 @@ func (c *typedClient) Update(ctx context.Context, obj Object, opts ...UpdateOpti } // Delete implements client.Client. -func (c *typedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { +func (c *typedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) (*unstructured.Unstructured, error) { o, err := c.resources.getObjMeta(obj) if err != nil { - return err + return nil, err } deleteOpts := DeleteOptions{} deleteOpts.ApplyOptions(opts) - return o.Delete(). + // directly deserializing into unstructured fails in the decoder + runtimeObj, err := o.Delete(). NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). Name(o.name). Body(deleteOpts.AsDeleteOptions()). Do(ctx). - Error() + Get() + if err != nil { + return nil, err + } + data, err := json.Marshal(runtimeObj) + if err != nil { + return nil, fmt.Errorf("failed to marshal delete response: %w", err) + } + response := &unstructured.Unstructured{} + if err := json.Unmarshal(data, &response.Object); err != nil { + return nil, fmt.Errorf("failed to unmarshal delete response: %w", err) + } + return response, nil } // DeleteAllOf implements client.Client. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/unstructured_client.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/unstructured_client.go index d2ea6d7a32..31e935e557 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/unstructured_client.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/unstructured_client.go @@ -21,12 +21,12 @@ import ( "fmt" "strings" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/util/apply" ) var _ Reader = &unstructuredClient{} -var _ Writer = &unstructuredClient{} type unstructuredClient struct { resources *clientRestResources @@ -93,26 +93,23 @@ func (uc *unstructuredClient) Update(ctx context.Context, obj Object, opts ...Up } // Delete implements client.Client. -func (uc *unstructuredClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error { - if _, ok := obj.(runtime.Unstructured); !ok { - return fmt.Errorf("unstructured client did not understand object: %T", obj) - } - +func (uc *unstructuredClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) (*unstructured.Unstructured, error) { o, err := uc.resources.getObjMeta(obj) if err != nil { - return err + return nil, err } deleteOpts := DeleteOptions{} deleteOpts.ApplyOptions(opts) - return o.Delete(). + response := &unstructured.Unstructured{} + return response, o.Delete(). NamespaceIfScoped(o.namespace, o.isNamespaced()). Resource(o.resource()). Name(o.name). Body(deleteOpts.AsDeleteOptions()). Do(ctx). - Error() + Into(response) } // DeleteAllOf implements client.Client. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/client/watch.go b/vendor/sigs.k8s.io/controller-runtime/pkg/client/watch.go index 181b22a673..6f1d81471d 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/client/watch.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/client/watch.go @@ -28,15 +28,16 @@ import ( // NewWithWatch returns a new WithWatch. func NewWithWatch(config *rest.Config, options Options) (WithWatch, error) { - client, err := newClient(config, options) + base, c, err := newClient(config, options) if err != nil { return nil, err } - return &watchingClient{client: client}, nil + return &watchingClient{Client: wrapClient(c, options), base: base}, nil } type watchingClient struct { - *client + Client + base *client } func (w *watchingClient) Watch(ctx context.Context, list ObjectList, opts ...ListOption) (watch.Interface, error) { @@ -67,7 +68,7 @@ func (w *watchingClient) metadataWatch(ctx context.Context, obj *metav1.PartialO listOpts := w.listOpts(opts...) - resInt, err := w.client.metadataClient.getResourceInterface(gvk, listOpts.Namespace) + resInt, err := w.base.metadataClient.getResourceInterface(gvk, listOpts.Namespace) if err != nil { return nil, err } @@ -76,7 +77,7 @@ func (w *watchingClient) metadataWatch(ctx context.Context, obj *metav1.PartialO } func (w *watchingClient) unstructuredWatch(ctx context.Context, obj runtime.Unstructured, opts ...ListOption) (watch.Interface, error) { - r, err := w.client.unstructuredClient.resources.getResource(obj) + r, err := w.base.unstructuredClient.resources.getResource(obj) if err != nil { return nil, err } @@ -86,12 +87,12 @@ func (w *watchingClient) unstructuredWatch(ctx context.Context, obj runtime.Unst return r.Get(). NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()). Resource(r.resource()). - VersionedParams(listOpts.AsListOptions(), w.client.unstructuredClient.paramCodec). + VersionedParams(listOpts.AsListOptions(), w.base.unstructuredClient.paramCodec). Watch(ctx) } func (w *watchingClient) typedWatch(ctx context.Context, obj ObjectList, opts ...ListOption) (watch.Interface, error) { - r, err := w.client.typedClient.resources.getResource(obj) + r, err := w.base.typedClient.resources.getResource(obj) if err != nil { return nil, err } @@ -101,6 +102,6 @@ func (w *watchingClient) typedWatch(ctx context.Context, obj ObjectList, opts .. return r.Get(). NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()). Resource(r.resource()). - VersionedParams(listOpts.AsListOptions(), w.client.typedClient.paramCodec). + VersionedParams(listOpts.AsListOptions(), w.base.typedClient.paramCodec). Watch(ctx) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/cluster/internal.go b/vendor/sigs.k8s.io/controller-runtime/pkg/cluster/internal.go index 755f83b546..e01f072694 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/cluster/internal.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/cluster/internal.go @@ -24,12 +24,12 @@ 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" "sigs.k8s.io/controller-runtime/pkg/client" intrec "sigs.k8s.io/controller-runtime/pkg/internal/recorder" + "sigs.k8s.io/controller-runtime/pkg/recorder" ) type cluster struct { @@ -88,7 +88,7 @@ func (c *cluster) GetEventRecorderFor(name string) record.EventRecorder { return c.recorderProvider.GetEventRecorderFor(name) } -func (c *cluster) GetEventRecorder(name string) events.EventRecorder { +func (c *cluster) GetEventRecorder(name string) recorder.EventRecorder { return c.recorderProvider.GetEventRecorder(name) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/config/controller.go b/vendor/sigs.k8s.io/controller-runtime/pkg/config/controller.go index 5eea2965f6..2877383f97 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/config/controller.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/config/controller.go @@ -44,7 +44,14 @@ type Controller struct { // e.g. ReplicaSet in apps group (regardless of version) would be `ReplicaSet.apps`. GroupKindConcurrency map[string]int - // MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1. + // MaxConcurrentReconciles is the maximum number of concurrent reconciliations which can be run. + // Defaults to 1. + // + // This value is used as a default for controllers that do not specify their own value. + // Per-controller options take precedence over this setting. + // + // For more details on how concurrency works, see the MaxConcurrentReconciles + // field in pkg/controller.TypedOptions. MaxConcurrentReconciles int // CacheSyncTimeout refers to the time limit set to wait for syncing caches. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controller.go b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controller.go index 853788d52f..1a5485f7b7 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controller.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/controller.go @@ -45,7 +45,26 @@ type TypedOptions[request comparable] struct { // Defaults to false if Controller.SkipNameValidation setting from the Manager is also unset. SkipNameValidation *bool - // MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1. + // MaxConcurrentReconciles is the maximum number of concurrent reconciliations + // that can be run. Defaults to 1. + // + // This value controls the number of worker goroutines that process items from + // the controller's workqueue. Increasing it allows different queue items to be + // reconciled in parallel, which can improve throughput when a controller manages + // many objects or when reconciliation involves slow operations such as external + // API calls. + // + // The workqueue ensures that the same item is not processed by multiple + // workers at the same time. If the same item is added again while it is being + // processed, it is processed again only after the current reconciliation + // finishes. For the default reconcile.Request type, the item key is the object's + // namespace/name. + // + // This option can also be configured at the manager level via + // config.Controller.MaxConcurrentReconciles or per GroupKind via + // config.Controller.GroupKindConcurrency (applied when using the builder + // utilities and no per-controller value is set). Per-controller values + // take precedence. MaxConcurrentReconciles int // CacheSyncTimeout refers to the time limit set to wait for syncing caches. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/metrics.go b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/metrics.go index 967a252dfb..14ab602de3 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/metrics.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/metrics.go @@ -1,3 +1,19 @@ +/* +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 priorityqueue import ( diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/priorityqueue.go b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/priorityqueue.go index 69ac25083b..dd267cd45a 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/priorityqueue.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue/priorityqueue.go @@ -1,3 +1,19 @@ +/* +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 priorityqueue import ( @@ -397,7 +413,16 @@ func (w *priorityqueue[T]) handleReadyItems() { w.waiters-- delete(w.items, item.Key) toDelete = append(toDelete, item) - w.get <- *item + // w.get is unbuffered, so this send blocks until a GetWithPriority + // consumer receives. A consumer that is parked in GetWithPriority can + // instead return via <-w.done once ShutDown closes it, leaving no one + // to receive here. Also watch w.done so the send does not block forever + // and deadlock the queue (the whole queue stalls because w.lock is held). + select { + case w.get <- *item: + case <-w.done: + return false + } return w.waiters > 0 }) diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go index 64cbe8a4d1..be6ba1faaa 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/handler/enqueue.go @@ -108,7 +108,7 @@ func (e *TypedEnqueueRequestForObject[T]) Generic(ctx context.Context, evt event } func isNil(arg any) bool { - if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr || + if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface || v.Kind() == reflect.Slice || v.Kind() == reflect.Map || diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go index bf81da39b6..3e7bd6ea31 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go @@ -58,6 +58,7 @@ type Options[request comparable] struct { NewQueue func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request] // MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1. + // See controller.TypedOptions.MaxConcurrentReconciles for full documentation. MaxConcurrentReconciles int // CacheSyncTimeout refers to the time limit set on waiting for cache to sync @@ -96,7 +97,8 @@ type Controller[request comparable] struct { // Name is used to uniquely identify a Controller in tracing, logging and monitoring. Name is required. Name string - // MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1. + // MaxConcurrentReconciles is the number of worker goroutines spawned to process work queue items. + // See controller.TypedOptions.MaxConcurrentReconciles for full documentation. MaxConcurrentReconciles int // Reconciler is a function that can be called at any time with the Name / Namespace of an object and @@ -277,6 +279,7 @@ func (c *Controller[request]) Start(ctx context.Context) error { // but lock outside to get proper handling of the queue shutdown c.mu.Lock() if c.Started { + c.mu.Unlock() return errors.New("controller was started more than once. This is likely to be caused by being added to a manager multiple times") } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics/metrics.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics/metrics.go index 39b435c453..1ece25c97f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics/metrics.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics/metrics.go @@ -32,7 +32,7 @@ var ( ReconcileTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "controller_runtime_reconcile_total", Help: "Total number of reconciliations per controller", - }, []string{"controller", "result"}) + }, []string{"controller", "result"}) //nolint:goconst // ReconcileErrors is a prometheus counter metrics which holds the total // number of errors from the Reconciler. diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/field/selector/utils.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/field/selector/utils.go index 8f6dc71ede..ffa3beba33 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/field/selector/utils.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/field/selector/utils.go @@ -5,7 +5,7 @@ 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 + 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, diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/httpserver/server.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/httpserver/server.go index 02fbe03cd9..d343fe27e7 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/httpserver/server.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/httpserver/server.go @@ -1,3 +1,19 @@ +/* +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 httpserver import ( diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/metrics/workqueue.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/metrics/workqueue.go index 94a98f96ed..35da4c1f2f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/metrics/workqueue.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/metrics/workqueue.go @@ -48,7 +48,7 @@ var ( Subsystem: WorkQueueSubsystem, Name: DepthKey, Help: "Current depth of workqueue by workqueue and priority", - }, []string{"name", "controller", "priority"}) + }, []string{"name", "controller", "priority"}) //nolint:goconst adds = prometheus.NewCounterVec(prometheus.CounterOpts{ Subsystem: WorkQueueSubsystem, diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/recorder/recorder.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/recorder/recorder.go index 4a1ccd7c0c..1f2738e5ed 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/recorder/recorder.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/recorder/recorder.go @@ -30,6 +30,7 @@ import ( "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/recorder" ) // EventBroadcasterProducer makes an event broadcaster, returning @@ -161,8 +162,11 @@ func (p *Provider) GetEventRecorderFor(name string) record.EventRecorder { } // 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 { +// broadcaster. All events will be associated with the given reportingController +// name. client-go appends "-" and the hostname to derive reportingInstance, +// which must fit within the events.k8s.io/v1 128-character limit. +// The returned recorder supports both Eventf and AnnotatedEventf. +func (p *Provider) GetEventRecorder(name string) recorder.EventRecorder { return &lazyRecorder{ prov: p, name: name, @@ -171,19 +175,24 @@ func (p *Provider) GetEventRecorder(name string) events.EventRecorder { // lazyRecorder is a recorder that doesn't actually instantiate any underlying // recorder until the first event is emitted. +var _ recorder.EventRecorder = (*lazyRecorder)(nil) + type lazyRecorder struct { prov *Provider name string - recOnce sync.Once - rec events.EventRecorder + recOnce sync.Once + eventRecorder events.EventRecorder + annotatedEventRecorder events.AnnotatedEventRecorder } // 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, l.name) + rec := broadcaster.NewRecorder(l.prov.scheme, l.name) + l.eventRecorder = rec + l.annotatedEventRecorder = rec.(events.AnnotatedEventRecorder) }) } @@ -192,7 +201,17 @@ func (l *lazyRecorder) Eventf(regarding runtime.Object, related runtime.Object, l.prov.lock.RLock() if !l.prov.stopped { - l.rec.Eventf(regarding, related, eventtype, reason, action, note, args...) + l.eventRecorder.Eventf(regarding, related, eventtype, reason, action, note, args...) + } + l.prov.lock.RUnlock() +} + +func (l *lazyRecorder) AnnotatedEventf(regarding runtime.Object, related runtime.Object, annotations map[string]string, eventtype, reason, action, note string, args ...any) { + l.ensureRecording() + + l.prov.lock.RLock() + if !l.prov.stopped { + l.annotatedEventRecorder.AnnotatedEventf(regarding, related, annotations, eventtype, reason, action, note, args...) } l.prov.lock.RUnlock() } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/event_handler.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/event_handler.go index 9d614f34a5..57a5cf98a1 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/event_handler.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/event_handler.go @@ -35,7 +35,7 @@ var log = logf.RuntimeLog.WithName("source").WithName("EventHandler") var _ cache.ResourceEventHandler = &EventHandler[client.Object, any]{} // NewEventHandler creates a new EventHandler. -func NewEventHandler[object client.Object, request comparable]( +func NewEventHandler[object any, request comparable]( ctx context.Context, queue workqueue.TypedRateLimitingInterface[request], handler handler.TypedEventHandler[object, request], @@ -49,7 +49,7 @@ func NewEventHandler[object client.Object, request comparable]( } // EventHandler adapts a handler.EventHandler interface to a cache.ResourceEventHandler interface. -type EventHandler[object client.Object, request comparable] struct { +type EventHandler[object any, request comparable] struct { // ctx stores the context that created the event handler // that is used to propagate cancellation signals to each handler function. ctx context.Context diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/kind.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/kind.go index 4eb82f9825..286d8094b7 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/kind.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/source/kind.go @@ -1,3 +1,19 @@ +/* +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 internal import ( @@ -148,7 +164,7 @@ func (ks *Kind[object, request]) WaitForSync(ctx context.Context) error { } func isNil(arg any) bool { - if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr || + if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface || v.Kind() == reflect.Slice || v.Kind() == reflect.Map || diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/syncs/syncs.go b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/syncs/syncs.go index c78a30377a..5e70f5ea01 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/internal/syncs/syncs.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/internal/syncs/syncs.go @@ -1,3 +1,19 @@ +/* +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 syncs import ( diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/internal.go b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/internal.go index ceb1450d1b..c029e7b48e 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/internal.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/internal.go @@ -32,7 +32,6 @@ 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" @@ -46,6 +45,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/internal/httpserver" intrec "sigs.k8s.io/controller-runtime/pkg/internal/recorder" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/recorder" "sigs.k8s.io/controller-runtime/pkg/webhook" ) @@ -264,7 +264,7 @@ func (cm *controllerManager) GetEventRecorderFor(name string) record.EventRecord return cm.cluster.GetEventRecorderFor(name) //nolint:staticcheck } -func (cm *controllerManager) GetEventRecorder(name string) events.EventRecorder { +func (cm *controllerManager) GetEventRecorder(name string) recorder.EventRecorder { return cm.cluster.GetEventRecorder(name) } @@ -462,6 +462,10 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) { // Create a context that inherits all keys from the parent context // but can be cancelled independently for leader election management baseCtx := context.WithoutCancel(ctx) + + // Inject the logger into the context for client-go contextual logging + baseCtx = logr.NewContext(baseCtx, cm.logger.WithName("leaderelection")) + leaderCtx, cancel := context.WithCancel(baseCtx) cm.leaderElectionCancel = cancel if leaderElector != nil { diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go index 53e29fc56f..da8889df4f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/runnable_group.go @@ -1,3 +1,19 @@ +/* +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 manager import ( diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/server.go b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/server.go index 1983165da8..cdf989a0e8 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/manager/server.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/manager/server.go @@ -5,7 +5,7 @@ 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 + 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, diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/client_go_adapter.go b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/client_go_adapter.go index ff28998c44..7d3d319704 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/client_go_adapter.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/client_go_adapter.go @@ -18,6 +18,10 @@ package metrics import ( "context" + "net/url" + "sync" + "sync/atomic" + "time" "github.com/prometheus/client_golang/prometheus" clientmetrics "k8s.io/client-go/tools/metrics" @@ -27,16 +31,134 @@ import ( // that client-go registers metrics. We copy the names and formats // from Kubernetes so that we match the core controllers. -var ( - // client metrics. +const ( + hostLabel = "host" + verbLabel = "verb" +) + +// defaultRESTClientDurationBuckets matches Kubernetes core controller REST client metrics. +// They start at 5ms; override via RESTClientMetricsOptions.DurationBuckets if a scrape +// pipeline still consumes classic buckets and needs sub-5ms resolution. +var defaultRESTClientDurationBuckets = []float64{0.005, 0.025, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 15.0, 30.0, 60.0} + +func restClientDurationHistogram(name, help string, labels []string, buckets []float64) *prometheus.HistogramVec { + if len(buckets) == 0 { + buckets = defaultRESTClientDurationBuckets + } + return prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: name, + Help: help, + Buckets: buckets, + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + NativeHistogramMinResetDuration: 1 * time.Hour, + }, labels) +} +func newRequestLatency(buckets []float64) *prometheus.HistogramVec { + return restClientDurationHistogram( + "rest_client_request_duration_seconds", + "Request latency in seconds. Broken down by verb and host.", + []string{verbLabel, hostLabel}, + buckets, + ) +} + +func newResolverLatency(buckets []float64) *prometheus.HistogramVec { + return restClientDurationHistogram( + "rest_client_dns_resolution_duration_seconds", + "DNS resolver latency in seconds. Broken down by host.", + []string{hostLabel}, + buckets, + ) +} + +func newRateLimiterLatency(buckets []float64) *prometheus.HistogramVec { + return restClientDurationHistogram( + "rest_client_rate_limiter_duration_seconds", + "Client side rate limiter latency in seconds. Broken down by verb, and host.", + []string{verbLabel, hostLabel}, + buckets, + ) +} + +func newRequestSize() *prometheus.HistogramVec { + return prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "rest_client_request_size_bytes", + Help: "Request size in bytes. Broken down by verb and host.", + // 64 bytes to 16MB + Buckets: []float64{64, 256, 512, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216}, + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + NativeHistogramMinResetDuration: 1 * time.Hour, + }, + []string{verbLabel, hostLabel}, + ) +} + +func newResponseSize() *prometheus.HistogramVec { + return prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "rest_client_response_size_bytes", + Help: "Response size in bytes. Broken down by verb and host.", + // 64 bytes to 16MB + Buckets: []float64{64, 256, 512, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216}, + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + NativeHistogramMinResetDuration: 1 * time.Hour, + }, + []string{verbLabel, hostLabel}, + ) +} + +func newRequestRetry() *prometheus.CounterVec { + return prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "rest_client_request_retries_total", + Help: "Number of request retries, partitioned by status code, verb, and host.", + }, + []string{"code", verbLabel, hostLabel}, + ) +} + +var ( + // requestResult is registered by default. The other client metrics are + // opt-in: adapters start with a nil collector and RegisterRESTClientMetrics* + // stores the HistogramVec / CounterVec. requestResult = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "rest_client_requests_total", Help: "Number of HTTP requests, partitioned by status code, method, and host.", }, - []string{"code", "method", "host"}, + []string{"code", "method", hostLabel}, ) + + requestLatency = &latencyAdapter{} + resolverLatency = &resolverLatencyAdapter{} + requestSize = &sizeAdapter{} + responseSize = &sizeAdapter{} + rateLimiterLatency = &latencyAdapter{} + requestRetry = &retryAdapter{} +) + +// RESTClientMetric identifies an opt-in client-go REST client metric. +// Pass values to RegisterRESTClientMetrics to enable a subset. +type RESTClientMetric int + +const ( + // MetricRequestLatency enables the rest_client_request_duration_seconds metric. + MetricRequestLatency = iota + 1 + // MetricDNSResolutionLatency enables the rest_client_dns_resolution_duration_seconds metric. + MetricDNSResolutionLatency + // MetricRequestSize enables the rest_client_request_size_bytes metric. + MetricRequestSize + // MetricResponseSize enables the rest_client_response_size_bytes metric. + MetricResponseSize + // MetricRateLimiterLatency enables the rest_client_rate_limiter_duration_seconds metric. + MetricRateLimiterLatency + // MetricRequestRetry enables the rest_client_request_retries_total metric. + MetricRequestRetry ) func init() { @@ -50,10 +172,66 @@ func registerClientMetrics() { // register the metrics with client-go clientmetrics.Register(clientmetrics.RegisterOpts{ - RequestResult: &resultAdapter{metric: requestResult}, + RequestResult: &resultAdapter{metric: requestResult}, + RequestLatency: requestLatency, + ResolverLatency: resolverLatency, + RequestSize: requestSize, + ResponseSize: responseSize, + RateLimiterLatency: rateLimiterLatency, + RequestRetry: requestRetry, }) } +// RESTClientMetricsOptions configures RegisterRESTClientMetricsWithOptions. +type RESTClientMetricsOptions struct { + // DurationBuckets overrides the classic Prometheus histogram buckets used by + // rest_client_request_duration_seconds, rest_client_dns_resolution_duration_seconds, + // and rest_client_rate_limiter_duration_seconds. + // + // If nil or empty, the Kubernetes-default buckets are kept (starting at 5ms). + // Native histogram settings are not changed. + // + // DurationBuckets is read when each duration metric is first registered. + // Later calls for the same metric are ignored so already-registered collectors + // are not replaced. + DurationBuckets []float64 +} + +// RegisterRESTClientMetrics enables the given client metrics using default buckets +// that match Kubernetes core controllers. +func RegisterRESTClientMetrics(metrics ...RESTClientMetric) { + RegisterRESTClientMetricsWithOptions(RESTClientMetricsOptions{}, metrics...) +} + +// RegisterRESTClientMetricsWithOptions enables the given client metrics. +// See RESTClientMetricsOptions for knobs such as custom duration buckets. +func RegisterRESTClientMetricsWithOptions(opts RESTClientMetricsOptions, metrics ...RESTClientMetric) { + for _, m := range metrics { + switch m { + case MetricRequestLatency: + requestLatency.enable(func() *prometheus.HistogramVec { + return newRequestLatency(opts.DurationBuckets) + }) + case MetricDNSResolutionLatency: + resolverLatency.enable(func() *prometheus.HistogramVec { + return newResolverLatency(opts.DurationBuckets) + }) + case MetricRequestSize: + requestSize.enable(newRequestSize) + case MetricResponseSize: + responseSize.enable(newResponseSize) + case MetricRateLimiterLatency: + rateLimiterLatency.enable(func() *prometheus.HistogramVec { + return newRateLimiterLatency(opts.DurationBuckets) + }) + case MetricRequestRetry: + requestRetry.enable(newRequestRetry) + default: + // unknown metric, ignore + } + } +} + // this section contains adapters, implementations, and other sundry organic, artisanally // hand-crafted syntax trees required to convince client-go that it actually wants to let // someone use its metrics. @@ -69,3 +247,87 @@ type resultAdapter struct { func (r *resultAdapter) Increment(_ context.Context, code, method, host string) { r.metric.WithLabelValues(code, method, host).Inc() } + +type latencyAdapter struct { + once sync.Once + metric atomic.Pointer[prometheus.HistogramVec] +} + +func (l *latencyAdapter) enable(newMetric func() *prometheus.HistogramVec) { + l.once.Do(func() { + h := newMetric() + l.metric.Store(h) + Registry.MustRegister(h) + }) +} + +func (l *latencyAdapter) Observe(_ context.Context, verb string, u url.URL, duration time.Duration) { + h := l.metric.Load() + if h == nil { + return + } + h.WithLabelValues(verb, u.Host).Observe(duration.Seconds()) +} + +type resolverLatencyAdapter struct { + once sync.Once + metric atomic.Pointer[prometheus.HistogramVec] +} + +func (r *resolverLatencyAdapter) enable(newMetric func() *prometheus.HistogramVec) { + r.once.Do(func() { + h := newMetric() + r.metric.Store(h) + Registry.MustRegister(h) + }) +} + +func (r *resolverLatencyAdapter) Observe(_ context.Context, host string, duration time.Duration) { + h := r.metric.Load() + if h == nil { + return + } + h.WithLabelValues(host).Observe(duration.Seconds()) +} + +type sizeAdapter struct { + once sync.Once + metric atomic.Pointer[prometheus.HistogramVec] +} + +func (r *sizeAdapter) enable(newMetric func() *prometheus.HistogramVec) { + r.once.Do(func() { + h := newMetric() + r.metric.Store(h) + Registry.MustRegister(h) + }) +} + +func (r *sizeAdapter) Observe(_ context.Context, verb string, host string, size float64) { + h := r.metric.Load() + if h == nil { + return + } + h.WithLabelValues(verb, host).Observe(size) +} + +type retryAdapter struct { + once sync.Once + metric atomic.Pointer[prometheus.CounterVec] +} + +func (r *retryAdapter) enable(newMetric func() *prometheus.CounterVec) { + r.once.Do(func() { + c := newMetric() + r.metric.Store(c) + Registry.MustRegister(c) + }) +} + +func (r *retryAdapter) IncrementRetry(_ context.Context, code, verb, host string) { + c := r.metric.Load() + if c == nil { + return + } + c.WithLabelValues(code, verb, host).Inc() +} diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/leaderelection.go b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/leaderelection.go index 61e1009d32..2253827b83 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/leaderelection.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/metrics/leaderelection.go @@ -1,3 +1,19 @@ +/* +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 ( diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/predicate/predicate.go b/vendor/sigs.k8s.io/controller-runtime/pkg/predicate/predicate.go index 9f24cb178c..d5086af3c3 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/predicate/predicate.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/predicate/predicate.go @@ -30,6 +30,9 @@ import ( var log = logf.RuntimeLog.WithName("predicate").WithName("eventFilters") // Predicate filters events before enqueuing the keys. +// +// NOTE: This does not affect what the cache stores. That is configured via +// cache.Options, which are global to all users of the cache. type Predicate = TypedPredicate[client.Object] // TypedPredicate filters events before enqueuing the keys. @@ -417,7 +420,7 @@ func LabelSelectorPredicate(s metav1.LabelSelector) (Predicate, error) { } func isNil(arg any) bool { - if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr || + if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface || v.Kind() == reflect.Slice || v.Kind() == reflect.Map || diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/recorder/recorder.go b/vendor/sigs.k8s.io/controller-runtime/pkg/recorder/recorder.go index b34fecb525..945202f81c 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/recorder/recorder.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/recorder/recorder.go @@ -25,12 +25,27 @@ import ( "k8s.io/client-go/tools/record" ) +// EventRecorder combines both events.EventRecorder and events.AnnotatedEventRecorder +// so that callers only need a single recorder to emit both regular and annotated events. +type EventRecorder interface { + events.EventRecorder + events.AnnotatedEventRecorder +} + // Provider knows how to generate new event recorders with given name. type Provider interface { // 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 + // GetEventRecorder returns an EventRecorder with given name. + // + // The name is used as the reportingController of events.k8s.io/v1 Events + // and must be a valid Kubernetes qualified name. client-go derives the + // reportingInstance by appending "-" and the current hostname to + // reportingController. reportingInstance must be no more than 128 + // characters, so callers should ensure that len(name) + 1 + len(hostname) + // is at most 128. + // The returned recorder supports both Eventf and AnnotatedEventf. + GetEventRecorder(name string) EventRecorder } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/source/source.go b/vendor/sigs.k8s.io/controller-runtime/pkg/source/source.go index e4f63ee5b5..8e17ad7577 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/source/source.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/source/source.go @@ -264,19 +264,23 @@ func (cs *channel[object, request]) syncLoop(ctx context.Context) { } } -// Informer is used to provide a source of events originating inside the cluster from Watches (e.g. Pod Create). -type Informer struct { +// TypedInformer is used to provide a source of events originating inside the cluster from Watches using generic +// event handlers and predicates. +type TypedInformer[object any, request comparable] struct { // Informer is the controller-runtime Informer Informer cache.Informer - Handler handler.EventHandler - Predicates []predicate.Predicate + Handler handler.TypedEventHandler[object, request] + Predicates []predicate.TypedPredicate[object] } +// Informer is used to provide a source of events originating inside the cluster from Watches (e.g. Pod Create). +type Informer = TypedInformer[client.Object, reconcile.Request] + var _ Source = &Informer{} // Start is internal and should be called only by the Controller to register an EventHandler with the Informer // to enqueue reconcile.Requests. -func (is *Informer) Start(ctx context.Context, queue workqueue.TypedRateLimitingInterface[reconcile.Request]) error { +func (is *TypedInformer[object, request]) Start(ctx context.Context, queue workqueue.TypedRateLimitingInterface[request]) error { // Informer should have been specified by the user. if is.Informer == nil { return fmt.Errorf("must specify Informer.Informer") @@ -294,7 +298,7 @@ func (is *Informer) Start(ctx context.Context, queue workqueue.TypedRateLimiting return nil } -func (is *Informer) String() string { +func (is *TypedInformer[object, request]) String() string { return fmt.Sprintf("informer source: %p", is.Informer) } diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/decode.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/decode.go index 576262cf70..535db0ab3f 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/decode.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/decode.go @@ -5,7 +5,7 @@ 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 + 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, diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter_custom.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter_custom.go index 9fec8003f2..da3bba50cd 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter_custom.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/defaulter_custom.go @@ -70,7 +70,7 @@ func WithDefaulter[T runtime.Object](scheme *runtime.Scheme, defaulter Defaulter new: func() T { var zero T typ := reflect.TypeOf(zero) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { return reflect.New(typ.Elem()).Interface().(T) } return zero diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go index ef9c456248..9156da2c24 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go @@ -5,7 +5,7 @@ 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 + 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, diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/response.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/response.go index ec1c88c989..4f05c0f463 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/response.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/response.go @@ -5,7 +5,7 @@ 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 + 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, diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go index f8401571d0..07a68c6a04 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_custom.go @@ -64,7 +64,7 @@ func WithValidator[T runtime.Object](scheme *runtime.Scheme, validator Validator new: func() T { var zero T typ := reflect.TypeOf(zero) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { return reflect.New(typ.Elem()).Interface().(T) } return zero diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/webhook.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/webhook.go index cba6da2cb0..4ac8e4a9c6 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/webhook.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/webhook.go @@ -5,7 +5,7 @@ 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 + 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, diff --git a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/server.go b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/server.go index 079f0a55ff..67dafb3456 100644 --- a/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/server.go +++ b/vendor/sigs.k8s.io/controller-runtime/pkg/webhook/server.go @@ -75,10 +75,18 @@ type Options struct { // Port is the port number that the server will serve. // It will be defaulted to 9443 if unspecified. + // + // To disable the webhook server set Port to -1. Port int - // CertDir is the directory that contains the server key and certificate. Defaults to - // /k8s-webhook-server/serving-certs. + // CertDir is the directory that contains the server key and certificate. + // The server expects the files to be named CertName and KeyName (see + // below). If unset, CertDir defaults to + // /k8s-webhook-server/serving-certs, where comes + // from os.TempDir() (usually /tmp on Linux, but TMPDIR-dependent). + // Most operators should configure this explicitly (for example, to the + // cert directory mounted in by a Kubernetes Secret) rather than relying + // on the default. CertDir string // CertName is the server certificate name. Defaults to tls.crt. @@ -136,7 +144,7 @@ func (o *Options) setDefaults() { o.WebhookMux = http.NewServeMux() } - if o.Port <= 0 { + if o.Port == 0 { o.Port = DefaultPort } @@ -180,7 +188,11 @@ func (s *DefaultServer) Register(path string, hook http.Handler) { s.webhookMux.Handle(path, metrics.InstrumentedHook(path, hook)) regLog := log.WithValues("path", path) - regLog.Info("Registering webhook") + if s.Options.Port < 0 { + regLog.Info("Webhook is disabled") + } else { + regLog.Info("Registering webhook") + } } // Start runs the server. @@ -188,6 +200,11 @@ func (s *DefaultServer) Register(path string, hook http.Handler) { func (s *DefaultServer) Start(ctx context.Context) error { s.defaultingOnce.Do(s.setDefaults) + if s.Options.Port < 0 { + log.Info("Webhook server is disabled") + return nil + } + log.Info("Starting webhook server") cfg := &tls.Config{ @@ -278,6 +295,9 @@ func (s *DefaultServer) StartedChecker() healthz.Checker { s.mu.Lock() defer s.mu.Unlock() + if s.Options.Port < 0 { + return nil + } if !s.started { return fmt.Errorf("webhook server has not been started yet") }