rebase: bump sigs.k8s.io/controller-runtime

Bumps the k8s-dependencies group with 1 update in the / directory: [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime).

Updates `sigs.k8s.io/controller-runtime` from 0.17.3 to 0.18.2
- [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.17.3...v0.18.2)

---
updated-dependencies:
- dependency-name: sigs.k8s.io/controller-runtime
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: k8s-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2024-05-13 20:57:03 +00:00
committed by mergify[bot]
parent c8af2b638a
commit c1ee11261e
60 changed files with 1559 additions and 1257 deletions

View File

@ -39,11 +39,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/cache/internal"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
)
var (
log = logf.RuntimeLog.WithName("object-cache")
defaultSyncPeriod = 10 * time.Hour
)
@ -203,6 +201,9 @@ type Options struct {
// DefaultTransform will be used as transform for all object types
// unless there is already one set in ByObject or DefaultNamespaces.
//
// A typical usecase for this is to use TransformStripManagedFields
// to reduce the caches memory usage.
DefaultTransform toolscache.TransformFunc
// DefaultWatchErrorHandler will be used to the WatchErrorHandler which is called
@ -222,7 +223,7 @@ type Options struct {
DefaultUnsafeDisableDeepCopy *bool
// ByObject restricts the cache's ListWatch to the desired fields per GVK at the specified object.
// object, this will fall through to Default* settings.
// If unset, this will fall through to the Default* settings.
ByObject map[client.Object]ByObject
// newInformer allows overriding of NewSharedIndexInformer for testing.
@ -346,6 +347,20 @@ func New(cfg *rest.Config, opts Options) (Cache, error) {
return delegating, nil
}
// TransformStripManagedFields strips the managed fields of an object before it is committed to the cache.
// If you are not explicitly accessing managedFields from your code, setting this as `DefaultTransform`
// on the cache can lead to a significant reduction in memory usage.
func TransformStripManagedFields() toolscache.TransformFunc {
return func(in any) (any, error) {
// Nilcheck managed fields to avoid hitting https://github.com/kubernetes/kubernetes/issues/124337
if obj, err := meta.Accessor(in); err == nil && obj.GetManagedFields() != nil {
obj.SetManagedFields(nil)
}
return in, nil
}
}
func optionDefaultsToConfig(opts *Options) Config {
return Config{
LabelSelector: opts.DefaultLabelSelector,
@ -419,19 +434,6 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
}
}
for namespace, cfg := range opts.DefaultNamespaces {
cfg = defaultConfig(cfg, optionDefaultsToConfig(&opts))
if namespace == metav1.NamespaceAll {
cfg.FieldSelector = fields.AndSelectors(
appendIfNotNil(
namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)),
cfg.FieldSelector,
)...,
)
}
opts.DefaultNamespaces[namespace] = cfg
}
for obj, byObject := range opts.ByObject {
isNamespaced, err := apiutil.IsObjectNamespaced(obj, opts.Scheme, opts.Mapper)
if err != nil {
@ -485,6 +487,22 @@ func defaultOpts(config *rest.Config, opts Options) (Options, error) {
opts.ByObject[obj] = byObject
}
// Default namespaces after byObject has been defaulted, otherwise a namespace without selectors
// will get the `Default` selectors, then get copied to byObject and then not get defaulted from
// byObject, as it already has selectors.
for namespace, cfg := range opts.DefaultNamespaces {
cfg = defaultConfig(cfg, optionDefaultsToConfig(&opts))
if namespace == metav1.NamespaceAll {
cfg.FieldSelector = fields.AndSelectors(
appendIfNotNil(
namespaceAllSelector(maps.Keys(opts.DefaultNamespaces)),
cfg.FieldSelector,
)...,
)
}
opts.DefaultNamespaces[namespace] = cfg
}
// Default the resync period to 10 hours if unset
if opts.SyncPeriod == nil {
opts.SyncPeriod = &defaultSyncPeriod

View File

@ -36,7 +36,7 @@ import (
// CacheReader is a client.Reader.
var _ client.Reader = &CacheReader{}
// CacheReader wraps a cache.Index to implement the client.CacheReader interface for a single type.
// 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.
indexer cache.Indexer

View File

@ -18,6 +18,7 @@ package internal
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
@ -186,10 +187,14 @@ type Informers struct {
// Start calls Run on each of the informers and sets started to true. Blocks on the context.
// It doesn't return start because it can't return an error, and it's not a runnable directly.
func (ip *Informers) Start(ctx context.Context) error {
func() {
if err := func() error {
ip.mu.Lock()
defer ip.mu.Unlock()
if ip.started {
return errors.New("Informer already started") //nolint:stylecheck
}
// Set the context so it can be passed to informers that are added later
ip.ctx = ctx
@ -207,7 +212,11 @@ func (ip *Informers) Start(ctx context.Context) error {
// Set started to true so we immediately start any informers added later.
ip.started = true
close(ip.startWait)
}()
return nil
}(); err != nil {
return err
}
<-ctx.Done() // Block until the context is done
ip.mu.Lock()
ip.stopped = true // Set stopped to true so we don't start any new informers

View File

@ -163,12 +163,13 @@ func (c *multiNamespaceCache) GetInformerForKind(ctx context.Context, gvk schema
}
func (c *multiNamespaceCache) Start(ctx context.Context) error {
errs := make(chan error)
// start global cache
if c.clusterCache != nil {
go func() {
err := c.clusterCache.Start(ctx)
if err != nil {
log.Error(err, "cluster scoped cache failed to start")
errs <- fmt.Errorf("failed to start cluster-scoped cache: %w", err)
}
}()
}
@ -177,13 +178,16 @@ func (c *multiNamespaceCache) Start(ctx context.Context) error {
for ns, cache := range c.namespaceToCache {
go func(ns string, cache Cache) {
if err := cache.Start(ctx); err != nil {
log.Error(err, "multi-namespace cache failed to start namespaced informer", "namespace", ns)
errs <- fmt.Errorf("failed to start cache for namespace %s: %w", ns, err)
}
}(ns, cache)
}
<-ctx.Done()
return nil
select {
case <-ctx.Done():
return nil
case err := <-errs:
return err
}
}
func (c *multiNamespaceCache) WaitForCacheSync(ctx context.Context) bool {

View File

@ -523,8 +523,8 @@ func (co *SubResourceCreateOptions) ApplyOptions(opts []SubResourceCreateOption)
return co
}
// ApplyToSubresourceCreate applies the the configuration on the given create options.
func (co *SubResourceCreateOptions) ApplyToSubresourceCreate(o *SubResourceCreateOptions) {
// ApplyToSubResourceCreate applies the the configuration on the given create options.
func (co *SubResourceCreateOptions) ApplyToSubResourceCreate(o *SubResourceCreateOptions) {
co.CreateOptions.ApplyToCreate(&co.CreateOptions)
}

View File

@ -0,0 +1,106 @@
/*
Copyright 2024 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"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// WithFieldOwner wraps a Client and adds the fieldOwner as the field
// manager to all write requests from this client. If additional [FieldOwner]
// options are specified on methods of this client, the value specified here
// will be overridden.
func WithFieldOwner(c Client, fieldOwner string) Client {
return &clientWithFieldManager{
owner: fieldOwner,
c: c,
Reader: c,
}
}
type clientWithFieldManager struct {
owner string
c Client
Reader
}
func (f *clientWithFieldManager) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
return f.c.Create(ctx, obj, append([]CreateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *clientWithFieldManager) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
return f.c.Update(ctx, obj, append([]UpdateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *clientWithFieldManager) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
return f.c.Patch(ctx, obj, patch, append([]PatchOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *clientWithFieldManager) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
return f.c.Delete(ctx, obj, opts...)
}
func (f *clientWithFieldManager) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
return f.c.DeleteAllOf(ctx, obj, opts...)
}
func (f *clientWithFieldManager) Scheme() *runtime.Scheme { return f.c.Scheme() }
func (f *clientWithFieldManager) RESTMapper() meta.RESTMapper { return f.c.RESTMapper() }
func (f *clientWithFieldManager) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
return f.c.GroupVersionKindFor(obj)
}
func (f *clientWithFieldManager) IsObjectNamespaced(obj runtime.Object) (bool, error) {
return f.c.IsObjectNamespaced(obj)
}
func (f *clientWithFieldManager) Status() StatusWriter {
return &subresourceClientWithFieldOwner{
owner: f.owner,
subresourceWriter: f.c.Status(),
}
}
func (f *clientWithFieldManager) SubResource(subresource string) SubResourceClient {
c := f.c.SubResource(subresource)
return &subresourceClientWithFieldOwner{
owner: f.owner,
subresourceWriter: c,
SubResourceReader: c,
}
}
type subresourceClientWithFieldOwner struct {
owner string
subresourceWriter SubResourceWriter
SubResourceReader
}
func (f *subresourceClientWithFieldOwner) Create(ctx context.Context, obj Object, subresource Object, opts ...SubResourceCreateOption) error {
return f.subresourceWriter.Create(ctx, obj, subresource, append([]SubResourceCreateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *subresourceClientWithFieldOwner) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
return f.subresourceWriter.Update(ctx, obj, append([]SubResourceUpdateOption{FieldOwner(f.owner)}, opts...)...)
}
func (f *subresourceClientWithFieldOwner) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
return f.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{FieldOwner(f.owner)}, opts...)...)
}

View File

@ -1,112 +0,0 @@
/*
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
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 config
import (
"fmt"
"os"
"sync"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
)
// ControllerManagerConfiguration defines the functions necessary to parse a config file
// and to configure the Options struct for the ctrl.Manager.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerManagerConfiguration interface {
runtime.Object
// Complete returns the versioned configuration
Complete() (v1alpha1.ControllerManagerConfigurationSpec, error)
}
// DeferredFileLoader is used to configure the decoder for loading controller
// runtime component config types.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type DeferredFileLoader struct {
ControllerManagerConfiguration
path string
scheme *runtime.Scheme
once sync.Once
err error
}
// File will set up the deferred file loader for the configuration
// this will also configure the defaults for the loader if nothing is
//
// Defaults:
// * Path: "./config.yaml"
// * Kind: GenericControllerManagerConfiguration
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
func File() *DeferredFileLoader {
scheme := runtime.NewScheme()
utilruntime.Must(v1alpha1.AddToScheme(scheme))
return &DeferredFileLoader{
path: "./config.yaml",
ControllerManagerConfiguration: &v1alpha1.ControllerManagerConfiguration{},
scheme: scheme,
}
}
// Complete will use sync.Once to set the scheme.
func (d *DeferredFileLoader) Complete() (v1alpha1.ControllerManagerConfigurationSpec, error) {
d.once.Do(d.loadFile)
if d.err != nil {
return v1alpha1.ControllerManagerConfigurationSpec{}, d.err
}
return d.ControllerManagerConfiguration.Complete()
}
// AtPath will set the path to load the file for the decoder.
func (d *DeferredFileLoader) AtPath(path string) *DeferredFileLoader {
d.path = path
return d
}
// OfKind will set the type to be used for decoding the file into.
func (d *DeferredFileLoader) OfKind(obj ControllerManagerConfiguration) *DeferredFileLoader {
d.ControllerManagerConfiguration = obj
return d
}
// loadFile is used from the mutex.Once to load the file.
func (d *DeferredFileLoader) loadFile() {
if d.scheme == nil {
d.err = fmt.Errorf("scheme not supplied to controller configuration loader")
return
}
content, err := os.ReadFile(d.path)
if err != nil {
d.err = fmt.Errorf("could not read file at %s", d.path)
return
}
codecs := serializer.NewCodecFactory(d.scheme)
// Regardless of if the bytes are of any external version,
// it will be read successfully and converted into the internal version
if err = runtime.DecodeInto(codecs.UniversalDecoder(), content, d.ControllerManagerConfiguration); err != nil {
d.err = fmt.Errorf("could not decode file into runtime.Object")
}
}

View File

@ -1,19 +0,0 @@
/*
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
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 config contains functionality for interacting with
// configuration for controller-runtime components.
package config

View File

@ -1,22 +0,0 @@
/*
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
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 v1alpha1 provides the ControllerManagerConfiguration used for
// configuring ctrl.Manager
// +kubebuilder:object:generate=true
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
package v1alpha1

View File

@ -1,43 +0,0 @@
/*
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
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 v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
GroupVersion = schema.GroupVersion{Group: "controller-runtime.sigs.k8s.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
AddToScheme = SchemeBuilder.AddToScheme
)
func init() {
SchemeBuilder.Register(&ControllerManagerConfiguration{})
}

View File

@ -1,179 +0,0 @@
/*
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
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 v1alpha1
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
configv1alpha1 "k8s.io/component-base/config/v1alpha1"
)
// ControllerManagerConfigurationSpec defines the desired state of GenericControllerManagerConfiguration.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerManagerConfigurationSpec struct {
// SyncPeriod determines the minimum frequency at which watched resources are
// reconciled. A lower period will correct entropy more quickly, but reduce
// responsiveness to change if there are many watched resources. Change this
// value only if you know what you are doing. Defaults to 10 hours if unset.
// there will a 10 percent jitter between the SyncPeriod of all controllers
// so that all controllers will not send list requests simultaneously.
// +optional
SyncPeriod *metav1.Duration `json:"syncPeriod,omitempty"`
// LeaderElection is the LeaderElection config to be used when configuring
// the manager.Manager leader election
// +optional
LeaderElection *configv1alpha1.LeaderElectionConfiguration `json:"leaderElection,omitempty"`
// CacheNamespace if specified restricts the manager's cache to watch objects in
// the desired namespace Defaults to all namespaces
//
// Note: If a namespace is specified, controllers can still Watch for a
// cluster-scoped resource (e.g Node). For namespaced resources the cache
// will only hold objects from the desired namespace.
// +optional
CacheNamespace string `json:"cacheNamespace,omitempty"`
// GracefulShutdownTimeout is the duration given to runnable to stop before the manager actually returns on stop.
// To disable graceful shutdown, set to time.Duration(0)
// To use graceful shutdown without timeout, set to a negative duration, e.G. time.Duration(-1)
// The graceful shutdown is skipped for safety reasons in case the leader election lease is lost.
GracefulShutdownTimeout *metav1.Duration `json:"gracefulShutDown,omitempty"`
// Controller contains global configuration options for controllers
// registered within this manager.
// +optional
Controller *ControllerConfigurationSpec `json:"controller,omitempty"`
// Metrics contains the controller metrics configuration
// +optional
Metrics ControllerMetrics `json:"metrics,omitempty"`
// Health contains the controller health configuration
// +optional
Health ControllerHealth `json:"health,omitempty"`
// Webhook contains the controllers webhook configuration
// +optional
Webhook ControllerWebhook `json:"webhook,omitempty"`
}
// ControllerConfigurationSpec defines the global configuration for
// controllers registered with the manager.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
//
// Deprecated: Controller global configuration can now be set at the manager level,
// using the manager.Options.Controller field.
type ControllerConfigurationSpec struct {
// GroupKindConcurrency is a map from a Kind to the number of concurrent reconciliation
// allowed for that controller.
//
// When a controller is registered within this manager using the builder utilities,
// users have to specify the type the controller reconciles in the For(...) call.
// If the object's kind passed matches one of the keys in this map, the concurrency
// for that controller is set to the number specified.
//
// The key is expected to be consistent in form with GroupKind.String(),
// e.g. ReplicaSet in apps group (regardless of version) would be `ReplicaSet.apps`.
//
// +optional
GroupKindConcurrency map[string]int `json:"groupKindConcurrency,omitempty"`
// CacheSyncTimeout refers to the time limit set to wait for syncing caches.
// Defaults to 2 minutes if not set.
// +optional
CacheSyncTimeout *time.Duration `json:"cacheSyncTimeout,omitempty"`
// RecoverPanic indicates if panics should be recovered.
// +optional
RecoverPanic *bool `json:"recoverPanic,omitempty"`
}
// ControllerMetrics defines the metrics configs.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerMetrics struct {
// BindAddress is the TCP address that the controller should bind to
// for serving prometheus metrics.
// It can be set to "0" to disable the metrics serving.
// +optional
BindAddress string `json:"bindAddress,omitempty"`
}
// ControllerHealth defines the health configs.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerHealth struct {
// HealthProbeBindAddress is the TCP address that the controller should bind to
// for serving health probes
// It can be set to "0" or "" to disable serving the health probe.
// +optional
HealthProbeBindAddress string `json:"healthProbeBindAddress,omitempty"`
// ReadinessEndpointName, defaults to "readyz"
// +optional
ReadinessEndpointName string `json:"readinessEndpointName,omitempty"`
// LivenessEndpointName, defaults to "healthz"
// +optional
LivenessEndpointName string `json:"livenessEndpointName,omitempty"`
}
// ControllerWebhook defines the webhook server for the controller.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerWebhook struct {
// Port is the port that the webhook server serves at.
// It is used to set webhook.Server.Port.
// +optional
Port *int `json:"port,omitempty"`
// Host is the hostname that the webhook server binds to.
// It is used to set webhook.Server.Host.
// +optional
Host string `json:"host,omitempty"`
// CertDir is the directory that contains the server key and certificate.
// if not set, webhook server would look up the server key and certificate in
// {TempDir}/k8s-webhook-server/serving-certs. The server key and certificate
// must be named tls.key and tls.crt, respectively.
// +optional
CertDir string `json:"certDir,omitempty"`
}
// +kubebuilder:object:root=true
// ControllerManagerConfiguration is the Schema for the GenericControllerManagerConfigurations API.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
type ControllerManagerConfiguration struct {
metav1.TypeMeta `json:",inline"`
// ControllerManagerConfiguration returns the contfigurations for controllers
ControllerManagerConfigurationSpec `json:",inline"`
}
// Complete returns the configuration for controller-runtime.
//
// Deprecated: The component config package has been deprecated and will be removed in a future release. Users should migrate to their own config implementation, please share feedback in https://github.com/kubernetes-sigs/controller-runtime/issues/895.
func (c *ControllerManagerConfigurationSpec) Complete() (ControllerManagerConfigurationSpec, error) {
return *c, nil
}

View File

@ -1,157 +0,0 @@
//go:build !ignore_autogenerated
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
configv1alpha1 "k8s.io/component-base/config/v1alpha1"
timex "time"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerConfigurationSpec) DeepCopyInto(out *ControllerConfigurationSpec) {
*out = *in
if in.GroupKindConcurrency != nil {
in, out := &in.GroupKindConcurrency, &out.GroupKindConcurrency
*out = make(map[string]int, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.CacheSyncTimeout != nil {
in, out := &in.CacheSyncTimeout, &out.CacheSyncTimeout
*out = new(timex.Duration)
**out = **in
}
if in.RecoverPanic != nil {
in, out := &in.RecoverPanic, &out.RecoverPanic
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerConfigurationSpec.
func (in *ControllerConfigurationSpec) DeepCopy() *ControllerConfigurationSpec {
if in == nil {
return nil
}
out := new(ControllerConfigurationSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerHealth) DeepCopyInto(out *ControllerHealth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerHealth.
func (in *ControllerHealth) DeepCopy() *ControllerHealth {
if in == nil {
return nil
}
out := new(ControllerHealth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerManagerConfiguration) DeepCopyInto(out *ControllerManagerConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ControllerManagerConfigurationSpec.DeepCopyInto(&out.ControllerManagerConfigurationSpec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerManagerConfiguration.
func (in *ControllerManagerConfiguration) DeepCopy() *ControllerManagerConfiguration {
if in == nil {
return nil
}
out := new(ControllerManagerConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ControllerManagerConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerManagerConfigurationSpec) DeepCopyInto(out *ControllerManagerConfigurationSpec) {
*out = *in
if in.SyncPeriod != nil {
in, out := &in.SyncPeriod, &out.SyncPeriod
*out = new(v1.Duration)
**out = **in
}
if in.LeaderElection != nil {
in, out := &in.LeaderElection, &out.LeaderElection
*out = new(configv1alpha1.LeaderElectionConfiguration)
(*in).DeepCopyInto(*out)
}
if in.GracefulShutdownTimeout != nil {
in, out := &in.GracefulShutdownTimeout, &out.GracefulShutdownTimeout
*out = new(v1.Duration)
**out = **in
}
if in.Controller != nil {
in, out := &in.Controller, &out.Controller
*out = new(ControllerConfigurationSpec)
(*in).DeepCopyInto(*out)
}
out.Metrics = in.Metrics
out.Health = in.Health
in.Webhook.DeepCopyInto(&out.Webhook)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerManagerConfigurationSpec.
func (in *ControllerManagerConfigurationSpec) DeepCopy() *ControllerManagerConfigurationSpec {
if in == nil {
return nil
}
out := new(ControllerManagerConfigurationSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerMetrics) DeepCopyInto(out *ControllerMetrics) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerMetrics.
func (in *ControllerMetrics) DeepCopy() *ControllerMetrics {
if in == nil {
return nil
}
out := new(ControllerMetrics)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ControllerWebhook) DeepCopyInto(out *ControllerWebhook) {
*out = *in
if in.Port != nil {
in, out := &in.Port, &out.Port
*out = new(int)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerWebhook.
func (in *ControllerWebhook) DeepCopy() *ControllerWebhook {
if in == nil {
return nil
}
out := new(ControllerWebhook)
in.DeepCopyInto(out)
return out
}

View File

@ -25,10 +25,8 @@ import (
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/internal/controller"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
@ -59,6 +57,18 @@ type Options struct {
// The overall is a token bucket and the per-item is exponential.
RateLimiter ratelimiter.RateLimiter
// NewQueue constructs the queue for this controller once the controller is ready to start.
// With NewQueue a custom queue implementation can be used, e.g. a priority queue to prioritize with which
// priority/order objects are reconciled (e.g. to reconcile objects with changes first).
// This is a func because the standard Kubernetes work queues start themselves immediately, which
// leads to goroutine leaks if something calls controller.New repeatedly.
// The NewQueue func gets the controller name and the RateLimiter option (defaulted if necessary) passed in.
// NewQueue defaults to NewRateLimitingQueueWithConfig.
//
// NOTE: LOW LEVEL PRIMITIVE!
// Only use a custom NewQueue if you know what you are doing.
NewQueue func(controllerName string, rateLimiter ratelimiter.RateLimiter) workqueue.RateLimitingInterface
// LogConstructor is used to construct a logger used for this controller and passed
// to each reconciliation via the context field.
LogConstructor func(request *reconcile.Request) logr.Logger
@ -72,13 +82,8 @@ type Controller interface {
// Reconciler is called to reconcile an object by Namespace/Name
reconcile.Reconciler
// Watch takes events provided by a Source and uses the EventHandler to
// enqueue reconcile.Requests in response to the events.
//
// Watch may be provided one or more Predicates to filter events before
// they are given to the EventHandler. Events will be passed to the
// EventHandler if all provided Predicates evaluate to true.
Watch(src source.Source, eventhandler handler.EventHandler, predicates ...predicate.Predicate) error
// Watch watches the provided Source.
Watch(src source.Source) error
// Start starts the controller. Start blocks until the context is closed or a
// controller has an error starting.
@ -147,6 +152,14 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
options.RateLimiter = workqueue.DefaultControllerRateLimiter()
}
if options.NewQueue == nil {
options.NewQueue = func(controllerName string, rateLimiter ratelimiter.RateLimiter) workqueue.RateLimitingInterface {
return workqueue.NewRateLimitingQueueWithConfig(rateLimiter, workqueue.RateLimitingQueueConfig{
Name: controllerName,
})
}
}
if options.RecoverPanic == nil {
options.RecoverPanic = mgr.GetControllerOptions().RecoverPanic
}
@ -157,12 +170,9 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
// Create controller with dependencies set
return &controller.Controller{
Do: options.Reconciler,
MakeQueue: func() workqueue.RateLimitingInterface {
return workqueue.NewRateLimitingQueueWithConfig(options.RateLimiter, workqueue.RateLimitingQueueConfig{
Name: name,
})
},
Do: options.Reconciler,
RateLimiter: options.RateLimiter,
NewQueue: options.NewQueue,
MaxConcurrentReconciles: options.MaxConcurrentReconciles,
CacheSyncTimeout: options.CacheSyncTimeout,
Name: name,

View File

@ -18,38 +18,55 @@ package event
import "sigs.k8s.io/controller-runtime/pkg/client"
// CreateEvent is an event where a Kubernetes object was created. CreateEvent should be generated
// CreateEvent is an event where a Kubernetes object was created. CreateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by a handler.EventHandler.
type CreateEvent = TypedCreateEvent[client.Object]
// UpdateEvent is an event where a Kubernetes object was updated. UpdateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type CreateEvent struct {
type UpdateEvent = TypedUpdateEvent[client.Object]
// DeleteEvent is an event where a Kubernetes object was deleted. DeleteEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type DeleteEvent = TypedDeleteEvent[client.Object]
// GenericEvent is an event where the operation type is unknown (e.g. polling or event originating outside the cluster).
// GenericEvent should be generated by a source.Source and transformed into a reconcile.Request by an
// handler.EventHandler.
type GenericEvent = TypedGenericEvent[client.Object]
// TypedCreateEvent is an event where a Kubernetes object was created. TypedCreateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.TypedEventHandler.
type TypedCreateEvent[T any] struct {
// Object is the object from the event
Object client.Object
Object T
}
// UpdateEvent is an event where a Kubernetes object was updated. UpdateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type UpdateEvent struct {
// TypedUpdateEvent is an event where a Kubernetes object was updated. TypedUpdateEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.TypedEventHandler.
type TypedUpdateEvent[T any] struct {
// ObjectOld is the object from the event
ObjectOld client.Object
ObjectOld T
// ObjectNew is the object from the event
ObjectNew client.Object
ObjectNew T
}
// DeleteEvent is an event where a Kubernetes object was deleted. DeleteEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.EventHandler.
type DeleteEvent struct {
// TypedDeleteEvent is an event where a Kubernetes object was deleted. TypedDeleteEvent should be generated
// by a source.Source and transformed into a reconcile.Request by an handler.TypedEventHandler.
type TypedDeleteEvent[T any] struct {
// Object is the object from the event
Object client.Object
Object T
// DeleteStateUnknown is true if the Delete event was missed but we identified the object
// as having been deleted.
DeleteStateUnknown bool
}
// GenericEvent is an event where the operation type is unknown (e.g. polling or event originating outside the cluster).
// GenericEvent should be generated by a source.Source and transformed into a reconcile.Request by an
// handler.EventHandler.
type GenericEvent struct {
// TypedGenericEvent is an event where the operation type is unknown (e.g. polling or event originating outside the cluster).
// TypedGenericEvent should be generated by a source.Source and transformed into a reconcile.Request by an
// handler.TypedEventHandler.
type TypedGenericEvent[T any] struct {
// Object is the object from the event
Object client.Object
Object T
}

View File

@ -18,9 +18,11 @@ package handler
import (
"context"
"reflect"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@ -33,13 +35,20 @@ type empty struct{}
var _ EventHandler = &EnqueueRequestForObject{}
// EnqueueRequestForObject enqueues a Request containing the Name and Namespace of the object that is the source of the Event.
// (e.g. the created / deleted / updated objects Name and Namespace). handler.EnqueueRequestForObject is used by almost all
// (e.g. the created / deleted / updated objects Name and Namespace). handler.EnqueueRequestForObject is used by almost all
// Controllers that have associated Resources (e.g. CRDs) to reconcile the associated Resource.
type EnqueueRequestForObject struct{}
type EnqueueRequestForObject = TypedEnqueueRequestForObject[client.Object]
// TypedEnqueueRequestForObject enqueues a Request containing the Name and Namespace of the object that is the source of the Event.
// (e.g. the created / deleted / updated objects Name and Namespace). handler.TypedEnqueueRequestForObject is used by almost all
// Controllers that have associated Resources (e.g. CRDs) to reconcile the associated Resource.
//
// TypedEnqueueRequestForObject is experimental and subject to future change.
type TypedEnqueueRequestForObject[T client.Object] struct{}
// Create implements EventHandler.
func (e *EnqueueRequestForObject) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) {
if evt.Object == nil {
func (e *TypedEnqueueRequestForObject[T]) Create(ctx context.Context, evt event.TypedCreateEvent[T], q workqueue.RateLimitingInterface) {
if isNil(evt.Object) {
enqueueLog.Error(nil, "CreateEvent received with no metadata", "event", evt)
return
}
@ -50,14 +59,14 @@ func (e *EnqueueRequestForObject) Create(ctx context.Context, evt event.CreateEv
}
// Update implements EventHandler.
func (e *EnqueueRequestForObject) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
func (e *TypedEnqueueRequestForObject[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[T], q workqueue.RateLimitingInterface) {
switch {
case evt.ObjectNew != nil:
case !isNil(evt.ObjectNew):
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.ObjectNew.GetName(),
Namespace: evt.ObjectNew.GetNamespace(),
}})
case evt.ObjectOld != nil:
case !isNil(evt.ObjectOld):
q.Add(reconcile.Request{NamespacedName: types.NamespacedName{
Name: evt.ObjectOld.GetName(),
Namespace: evt.ObjectOld.GetNamespace(),
@ -68,8 +77,8 @@ func (e *EnqueueRequestForObject) Update(ctx context.Context, evt event.UpdateEv
}
// Delete implements EventHandler.
func (e *EnqueueRequestForObject) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
if evt.Object == nil {
func (e *TypedEnqueueRequestForObject[T]) Delete(ctx context.Context, evt event.TypedDeleteEvent[T], q workqueue.RateLimitingInterface) {
if isNil(evt.Object) {
enqueueLog.Error(nil, "DeleteEvent received with no metadata", "event", evt)
return
}
@ -80,8 +89,8 @@ func (e *EnqueueRequestForObject) Delete(ctx context.Context, evt event.DeleteEv
}
// Generic implements EventHandler.
func (e *EnqueueRequestForObject) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) {
if evt.Object == nil {
func (e *TypedEnqueueRequestForObject[T]) Generic(ctx context.Context, evt event.TypedGenericEvent[T], q workqueue.RateLimitingInterface) {
if isNil(evt.Object) {
enqueueLog.Error(nil, "GenericEvent received with no metadata", "event", evt)
return
}
@ -90,3 +99,15 @@ func (e *EnqueueRequestForObject) Generic(ctx context.Context, evt event.Generic
Namespace: evt.Object.GetNamespace(),
}})
}
func isNil(arg any) bool {
if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr ||
v.Kind() == reflect.Interface ||
v.Kind() == reflect.Slice ||
v.Kind() == reflect.Map ||
v.Kind() == reflect.Chan ||
v.Kind() == reflect.Func) && v.IsNil()) {
return true
}
return false
}

View File

@ -27,7 +27,13 @@ import (
// MapFunc is the signature required for enqueueing requests from a generic function.
// This type is usually used with EnqueueRequestsFromMapFunc when registering an event handler.
type MapFunc func(context.Context, client.Object) []reconcile.Request
type MapFunc = TypedMapFunc[client.Object]
// TypedMapFunc is the signature required for enqueueing requests from a generic function.
// This type is usually used with EnqueueRequestsFromTypedMapFunc when registering an event handler.
//
// TypedMapFunc is experimental and subject to future change.
type TypedMapFunc[T any] func(context.Context, T) []reconcile.Request
// EnqueueRequestsFromMapFunc enqueues Requests by running a transformation function that outputs a collection
// of reconcile.Requests on each Event. The reconcile.Requests may be for an arbitrary set of objects
@ -40,44 +46,60 @@ type MapFunc func(context.Context, client.Object) []reconcile.Request
// For UpdateEvents which contain both a new and old object, the transformation function is run on both
// objects and both sets of Requests are enqueue.
func EnqueueRequestsFromMapFunc(fn MapFunc) EventHandler {
return &enqueueRequestsFromMapFunc{
return TypedEnqueueRequestsFromMapFunc(fn)
}
// TypedEnqueueRequestsFromMapFunc enqueues Requests by running a transformation function that outputs a collection
// of reconcile.Requests on each Event. The reconcile.Requests may be for an arbitrary set of objects
// defined by some user specified transformation of the source Event. (e.g. trigger Reconciler for a set of objects
// in response to a cluster resize event caused by adding or deleting a Node)
//
// TypedEnqueueRequestsFromMapFunc is frequently used to fan-out updates from one object to one or more other
// objects of a differing type.
//
// For TypedUpdateEvents which contain both a new and old object, the transformation function is run on both
// objects and both sets of Requests are enqueue.
//
// TypedEnqueueRequestsFromMapFunc is experimental and subject to future change.
func TypedEnqueueRequestsFromMapFunc[T any](fn TypedMapFunc[T]) TypedEventHandler[T] {
return &enqueueRequestsFromMapFunc[T]{
toRequests: fn,
}
}
var _ EventHandler = &enqueueRequestsFromMapFunc{}
var _ EventHandler = &enqueueRequestsFromMapFunc[client.Object]{}
type enqueueRequestsFromMapFunc struct {
type enqueueRequestsFromMapFunc[T any] struct {
// Mapper transforms the argument into a slice of keys to be reconciled
toRequests MapFunc
toRequests TypedMapFunc[T]
}
// Create implements EventHandler.
func (e *enqueueRequestsFromMapFunc) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestsFromMapFunc[T]) Create(ctx context.Context, evt event.TypedCreateEvent[T], q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
e.mapAndEnqueue(ctx, q, evt.Object, reqs)
}
// Update implements EventHandler.
func (e *enqueueRequestsFromMapFunc) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestsFromMapFunc[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[T], q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
e.mapAndEnqueue(ctx, q, evt.ObjectOld, reqs)
e.mapAndEnqueue(ctx, q, evt.ObjectNew, reqs)
}
// Delete implements EventHandler.
func (e *enqueueRequestsFromMapFunc) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestsFromMapFunc[T]) Delete(ctx context.Context, evt event.TypedDeleteEvent[T], q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
e.mapAndEnqueue(ctx, q, evt.Object, reqs)
}
// Generic implements EventHandler.
func (e *enqueueRequestsFromMapFunc) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestsFromMapFunc[T]) Generic(ctx context.Context, evt event.TypedGenericEvent[T], q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
e.mapAndEnqueue(ctx, q, evt.Object, reqs)
}
func (e *enqueueRequestsFromMapFunc) mapAndEnqueue(ctx context.Context, q workqueue.RateLimitingInterface, object client.Object, reqs map[reconcile.Request]empty) {
func (e *enqueueRequestsFromMapFunc[T]) mapAndEnqueue(ctx context.Context, q workqueue.RateLimitingInterface, object T, reqs map[reconcile.Request]empty) {
for _, req := range e.toRequests(ctx, object) {
_, ok := reqs[req]
if !ok {

View File

@ -32,12 +32,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
var _ EventHandler = &enqueueRequestForOwner{}
var _ EventHandler = &enqueueRequestForOwner[client.Object]{}
var log = logf.RuntimeLog.WithName("eventhandler").WithName("enqueueRequestForOwner")
// OwnerOption modifies an EnqueueRequestForOwner EventHandler.
type OwnerOption func(e *enqueueRequestForOwner)
type OwnerOption func(e enqueueRequestForOwnerInterface)
// EnqueueRequestForOwner enqueues Requests for the Owners of an object. E.g. the object that created
// the object that was the source of the Event.
@ -48,7 +48,21 @@ type OwnerOption func(e *enqueueRequestForOwner)
//
// - a handler.enqueueRequestForOwner EventHandler with an OwnerType of ReplicaSet and OnlyControllerOwner set to true.
func EnqueueRequestForOwner(scheme *runtime.Scheme, mapper meta.RESTMapper, ownerType client.Object, opts ...OwnerOption) EventHandler {
e := &enqueueRequestForOwner{
return TypedEnqueueRequestForOwner[client.Object](scheme, mapper, ownerType, opts...)
}
// TypedEnqueueRequestForOwner enqueues Requests for the Owners of an object. E.g. the object that created
// the object that was the source of the Event.
//
// If a ReplicaSet creates Pods, users may reconcile the ReplicaSet in response to Pod Events using:
//
// - a source.Kind Source with Type of Pod.
//
// - a handler.typedEnqueueRequestForOwner EventHandler with an OwnerType of ReplicaSet and OnlyControllerOwner set to true.
//
// TypedEnqueueRequestForOwner is experimental and subject to future change.
func TypedEnqueueRequestForOwner[T client.Object](scheme *runtime.Scheme, mapper meta.RESTMapper, ownerType client.Object, opts ...OwnerOption) TypedEventHandler[T] {
e := &enqueueRequestForOwner[T]{
ownerType: ownerType,
mapper: mapper,
}
@ -63,12 +77,16 @@ func EnqueueRequestForOwner(scheme *runtime.Scheme, mapper meta.RESTMapper, owne
// OnlyControllerOwner if provided will only look at the first OwnerReference with Controller: true.
func OnlyControllerOwner() OwnerOption {
return func(e *enqueueRequestForOwner) {
e.isController = true
return func(e enqueueRequestForOwnerInterface) {
e.setIsController(true)
}
}
type enqueueRequestForOwner struct {
type enqueueRequestForOwnerInterface interface {
setIsController(bool)
}
type enqueueRequestForOwner[T client.Object] struct {
// ownerType is the type of the Owner object to look for in OwnerReferences. Only Group and Kind are compared.
ownerType runtime.Object
@ -82,8 +100,12 @@ type enqueueRequestForOwner struct {
mapper meta.RESTMapper
}
func (e *enqueueRequestForOwner[T]) setIsController(isController bool) {
e.isController = isController
}
// Create implements EventHandler.
func (e *enqueueRequestForOwner) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestForOwner[T]) Create(ctx context.Context, evt event.TypedCreateEvent[T], q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
e.getOwnerReconcileRequest(evt.Object, reqs)
for req := range reqs {
@ -92,7 +114,7 @@ func (e *enqueueRequestForOwner) Create(ctx context.Context, evt event.CreateEve
}
// Update implements EventHandler.
func (e *enqueueRequestForOwner) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestForOwner[T]) Update(ctx context.Context, evt event.TypedUpdateEvent[T], q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
e.getOwnerReconcileRequest(evt.ObjectOld, reqs)
e.getOwnerReconcileRequest(evt.ObjectNew, reqs)
@ -102,7 +124,7 @@ func (e *enqueueRequestForOwner) Update(ctx context.Context, evt event.UpdateEve
}
// Delete implements EventHandler.
func (e *enqueueRequestForOwner) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestForOwner[T]) Delete(ctx context.Context, evt event.TypedDeleteEvent[T], q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
e.getOwnerReconcileRequest(evt.Object, reqs)
for req := range reqs {
@ -111,7 +133,7 @@ func (e *enqueueRequestForOwner) Delete(ctx context.Context, evt event.DeleteEve
}
// Generic implements EventHandler.
func (e *enqueueRequestForOwner) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) {
func (e *enqueueRequestForOwner[T]) Generic(ctx context.Context, evt event.TypedGenericEvent[T], q workqueue.RateLimitingInterface) {
reqs := map[reconcile.Request]empty{}
e.getOwnerReconcileRequest(evt.Object, reqs)
for req := range reqs {
@ -121,7 +143,7 @@ func (e *enqueueRequestForOwner) Generic(ctx context.Context, evt event.GenericE
// parseOwnerTypeGroupKind parses the OwnerType into a Group and Kind and caches the result. Returns false
// if the OwnerType could not be parsed using the scheme.
func (e *enqueueRequestForOwner) parseOwnerTypeGroupKind(scheme *runtime.Scheme) error {
func (e *enqueueRequestForOwner[T]) parseOwnerTypeGroupKind(scheme *runtime.Scheme) error {
// Get the kinds of the type
kinds, _, err := scheme.ObjectKinds(e.ownerType)
if err != nil {
@ -141,7 +163,7 @@ func (e *enqueueRequestForOwner) parseOwnerTypeGroupKind(scheme *runtime.Scheme)
// getOwnerReconcileRequest looks at object and builds a map of reconcile.Request to reconcile
// owners of object that match e.OwnerType.
func (e *enqueueRequestForOwner) getOwnerReconcileRequest(object metav1.Object, result map[reconcile.Request]empty) {
func (e *enqueueRequestForOwner[T]) getOwnerReconcileRequest(object metav1.Object, result map[reconcile.Request]empty) {
// Iterate through the OwnerReferences looking for a match on Group and Kind against what was requested
// by the user
for _, ref := range e.getOwnersReferences(object) {
@ -181,7 +203,7 @@ func (e *enqueueRequestForOwner) getOwnerReconcileRequest(object metav1.Object,
// getOwnersReferences returns the OwnerReferences for an object as specified by the enqueueRequestForOwner
// - if IsController is true: only take the Controller OwnerReference (if found)
// - if IsController is false: take all OwnerReferences.
func (e *enqueueRequestForOwner) getOwnersReferences(object metav1.Object) []metav1.OwnerReference {
func (e *enqueueRequestForOwner[T]) getOwnersReferences(object metav1.Object) []metav1.OwnerReference {
if object == nil {
return nil
}

View File

@ -20,12 +20,13 @@ import (
"context"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
)
// EventHandler enqueues reconcile.Requests in response to events (e.g. Pod Create). EventHandlers map an Event
// for one object to trigger Reconciles for either the same object or different objects - e.g. if there is an
// Event for object with type Foo (using source.KindSource) then reconcile one or more object(s) with type Bar.
// Event for object with type Foo (using source.Kind) then reconcile one or more object(s) with type Bar.
//
// Identical reconcile.Requests will be batched together through the queuing mechanism before reconcile is called.
//
@ -41,65 +42,92 @@ import (
//
// Unless you are implementing your own EventHandler, you can ignore the functions on the EventHandler interface.
// Most users shouldn't need to implement their own EventHandler.
type EventHandler interface {
type EventHandler TypedEventHandler[client.Object]
// TypedEventHandler enqueues reconcile.Requests in response to events (e.g. Pod Create). TypedEventHandlers map an Event
// for one object to trigger Reconciles for either the same object or different objects - e.g. if there is an
// Event for object with type Foo (using source.Kind) then reconcile one or more object(s) with type Bar.
//
// Identical reconcile.Requests will be batched together through the queuing mechanism before reconcile is called.
//
// * Use TypedEnqueueRequestForObject to reconcile the object the event is for
// - do this for events for the type the Controller Reconciles. (e.g. Deployment for a Deployment Controller)
//
// * Use TypedEnqueueRequestForOwner to reconcile the owner of the object the event is for
// - do this for events for the types the Controller creates. (e.g. ReplicaSets created by a Deployment Controller)
//
// * Use TypedEnqueueRequestsFromMapFunc to transform an event for an object to a reconcile of an object
// of a different type - do this for events for types the Controller may be interested in, but doesn't create.
// (e.g. If Foo responds to cluster size events, map Node events to Foo objects.)
//
// Unless you are implementing your own TypedEventHandler, you can ignore the functions on the TypedEventHandler interface.
// Most users shouldn't need to implement their own TypedEventHandler.
//
// TypedEventHandler is experimental and subject to future change.
type TypedEventHandler[T any] interface {
// Create is called in response to a create event - e.g. Pod Creation.
Create(context.Context, event.CreateEvent, workqueue.RateLimitingInterface)
Create(context.Context, event.TypedCreateEvent[T], workqueue.RateLimitingInterface)
// Update is called in response to an update event - e.g. Pod Updated.
Update(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface)
Update(context.Context, event.TypedUpdateEvent[T], workqueue.RateLimitingInterface)
// Delete is called in response to a delete event - e.g. Pod Deleted.
Delete(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface)
Delete(context.Context, event.TypedDeleteEvent[T], workqueue.RateLimitingInterface)
// Generic is called in response to an event of an unknown type or a synthetic event triggered as a cron or
// external trigger request - e.g. reconcile Autoscaling, or a Webhook.
Generic(context.Context, event.GenericEvent, workqueue.RateLimitingInterface)
Generic(context.Context, event.TypedGenericEvent[T], workqueue.RateLimitingInterface)
}
var _ EventHandler = Funcs{}
// Funcs implements EventHandler.
type Funcs struct {
// Funcs implements eventhandler.
type Funcs = TypedFuncs[client.Object]
// TypedFuncs implements eventhandler.
//
// TypedFuncs is experimental and subject to future change.
type TypedFuncs[T any] struct {
// Create is called in response to an add event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
CreateFunc func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface)
CreateFunc func(context.Context, event.TypedCreateEvent[T], workqueue.RateLimitingInterface)
// Update is called in response to an update event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
UpdateFunc func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface)
UpdateFunc func(context.Context, event.TypedUpdateEvent[T], workqueue.RateLimitingInterface)
// Delete is called in response to a delete event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
DeleteFunc func(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface)
DeleteFunc func(context.Context, event.TypedDeleteEvent[T], workqueue.RateLimitingInterface)
// GenericFunc is called in response to a generic event. Defaults to no-op.
// RateLimitingInterface is used to enqueue reconcile.Requests.
GenericFunc func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface)
GenericFunc func(context.Context, event.TypedGenericEvent[T], workqueue.RateLimitingInterface)
}
// Create implements EventHandler.
func (h Funcs) Create(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) {
func (h TypedFuncs[T]) Create(ctx context.Context, e event.TypedCreateEvent[T], q workqueue.RateLimitingInterface) {
if h.CreateFunc != nil {
h.CreateFunc(ctx, e, q)
}
}
// Delete implements EventHandler.
func (h Funcs) Delete(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) {
func (h TypedFuncs[T]) Delete(ctx context.Context, e event.TypedDeleteEvent[T], q workqueue.RateLimitingInterface) {
if h.DeleteFunc != nil {
h.DeleteFunc(ctx, e, q)
}
}
// Update implements EventHandler.
func (h Funcs) Update(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) {
func (h TypedFuncs[T]) Update(ctx context.Context, e event.TypedUpdateEvent[T], q workqueue.RateLimitingInterface) {
if h.UpdateFunc != nil {
h.UpdateFunc(ctx, e, q)
}
}
// Generic implements EventHandler.
func (h Funcs) Generic(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) {
func (h TypedFuncs[T]) Generic(ctx context.Context, e event.TypedGenericEvent[T], q workqueue.RateLimitingInterface) {
if h.GenericFunc != nil {
h.GenericFunc(ctx, e, q)
}

View File

@ -29,10 +29,9 @@ import (
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/handler"
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
@ -50,10 +49,13 @@ type Controller struct {
// Defaults to the DefaultReconcileFunc.
Do reconcile.Reconciler
// MakeQueue constructs the queue for this controller once the controller is ready to start.
// This exists because the standard Kubernetes workqueues start themselves immediately, which
// RateLimiter is used to limit how frequently requests may be queued into the work queue.
RateLimiter ratelimiter.RateLimiter
// NewQueue constructs the queue for this controller once the controller is ready to start.
// This is a func because the standard Kubernetes work queues start themselves immediately, which
// leads to goroutine leaks if something calls controller.New repeatedly.
MakeQueue func() workqueue.RateLimitingInterface
NewQueue func(controllerName string, rateLimiter ratelimiter.RateLimiter) workqueue.RateLimitingInterface
// Queue is an listeningQueue that listens for events from Informers and adds object keys to
// the Queue for processing
@ -77,7 +79,7 @@ type Controller struct {
CacheSyncTimeout time.Duration
// startWatches maintains a list of sources, handlers, and predicates to start when the controller is started.
startWatches []watchDescription
startWatches []source.Source
// LogConstructor is used to construct a logger to then log messages to users during reconciliation,
// or for example when a watch is started.
@ -92,13 +94,6 @@ type Controller struct {
LeaderElected *bool
}
// watchDescription contains all the information necessary to start a watch.
type watchDescription struct {
src source.Source
handler handler.EventHandler
predicates []predicate.Predicate
}
// Reconcile implements reconcile.Reconciler.
func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, err error) {
defer func() {
@ -120,7 +115,7 @@ func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (_ re
}
// Watch implements controller.Controller.
func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prct ...predicate.Predicate) error {
func (c *Controller) Watch(src source.Source) error {
c.mu.Lock()
defer c.mu.Unlock()
@ -128,12 +123,12 @@ func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prc
//
// These watches are going to be held on the controller struct until the manager or user calls Start(...).
if !c.Started {
c.startWatches = append(c.startWatches, watchDescription{src: src, handler: evthdler, predicates: prct})
c.startWatches = append(c.startWatches, src)
return nil
}
c.LogConstructor(nil).Info("Starting EventSource", "source", src)
return src.Start(c.ctx, evthdler, c.Queue, prct...)
return src.Start(c.ctx, c.Queue)
}
// NeedLeaderElection implements the manager.LeaderElectionRunnable interface.
@ -158,7 +153,7 @@ func (c *Controller) Start(ctx context.Context) error {
// Set the internal context.
c.ctx = ctx
c.Queue = c.MakeQueue()
c.Queue = c.NewQueue(c.Name, c.RateLimiter)
go func() {
<-ctx.Done()
c.Queue.ShutDown()
@ -175,9 +170,9 @@ func (c *Controller) Start(ctx context.Context) error {
// caches to sync so that they have a chance to register their intendeded
// caches.
for _, watch := range c.startWatches {
c.LogConstructor(nil).Info("Starting EventSource", "source", fmt.Sprintf("%s", watch.src))
c.LogConstructor(nil).Info("Starting EventSource", "source", fmt.Sprintf("%s", watch))
if err := watch.src.Start(ctx, watch.handler, c.Queue, watch.predicates...); err != nil {
if err := watch.Start(ctx, c.Queue); err != nil {
return err
}
}
@ -186,7 +181,7 @@ func (c *Controller) Start(ctx context.Context) error {
c.LogConstructor(nil).Info("Starting Controller")
for _, watch := range c.startWatches {
syncingSource, ok := watch.src.(source.SyncingSource)
syncingSource, ok := watch.(source.SyncingSource)
if !ok {
continue
}

View File

@ -33,8 +33,8 @@ import (
var log = logf.RuntimeLog.WithName("source").WithName("EventHandler")
// NewEventHandler creates a new EventHandler.
func NewEventHandler(ctx context.Context, queue workqueue.RateLimitingInterface, handler handler.EventHandler, predicates []predicate.Predicate) *EventHandler {
return &EventHandler{
func NewEventHandler[T client.Object](ctx context.Context, queue workqueue.RateLimitingInterface, handler handler.TypedEventHandler[T], predicates []predicate.TypedPredicate[T]) *EventHandler[T] {
return &EventHandler[T]{
ctx: ctx,
handler: handler,
queue: queue,
@ -43,19 +43,19 @@ func NewEventHandler(ctx context.Context, queue workqueue.RateLimitingInterface,
}
// EventHandler adapts a handler.EventHandler interface to a cache.ResourceEventHandler interface.
type EventHandler struct {
type EventHandler[T client.Object] struct {
// ctx stores the context that created the event handler
// that is used to propagate cancellation signals to each handler function.
ctx context.Context
handler handler.EventHandler
handler handler.TypedEventHandler[T]
queue workqueue.RateLimitingInterface
predicates []predicate.Predicate
predicates []predicate.TypedPredicate[T]
}
// HandlerFuncs converts EventHandler to a ResourceEventHandlerFuncs
// TODO: switch to ResourceEventHandlerDetailedFuncs with client-go 1.27
func (e *EventHandler) HandlerFuncs() cache.ResourceEventHandlerFuncs {
func (e *EventHandler[T]) HandlerFuncs() cache.ResourceEventHandlerFuncs {
return cache.ResourceEventHandlerFuncs{
AddFunc: e.OnAdd,
UpdateFunc: e.OnUpdate,
@ -64,11 +64,11 @@ func (e *EventHandler) HandlerFuncs() cache.ResourceEventHandlerFuncs {
}
// OnAdd creates CreateEvent and calls Create on EventHandler.
func (e *EventHandler) OnAdd(obj interface{}) {
c := event.CreateEvent{}
func (e *EventHandler[T]) OnAdd(obj interface{}) {
c := event.TypedCreateEvent[T]{}
// Pull Object out of the object
if o, ok := obj.(client.Object); ok {
if o, ok := obj.(T); ok {
c.Object = o
} else {
log.Error(nil, "OnAdd missing Object",
@ -89,10 +89,10 @@ func (e *EventHandler) OnAdd(obj interface{}) {
}
// OnUpdate creates UpdateEvent and calls Update on EventHandler.
func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) {
u := event.UpdateEvent{}
func (e *EventHandler[T]) OnUpdate(oldObj, newObj interface{}) {
u := event.TypedUpdateEvent[T]{}
if o, ok := oldObj.(client.Object); ok {
if o, ok := oldObj.(T); ok {
u.ObjectOld = o
} else {
log.Error(nil, "OnUpdate missing ObjectOld",
@ -101,7 +101,7 @@ func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) {
}
// Pull Object out of the object
if o, ok := newObj.(client.Object); ok {
if o, ok := newObj.(T); ok {
u.ObjectNew = o
} else {
log.Error(nil, "OnUpdate missing ObjectNew",
@ -122,8 +122,8 @@ func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) {
}
// OnDelete creates DeleteEvent and calls Delete on EventHandler.
func (e *EventHandler) OnDelete(obj interface{}) {
d := event.DeleteEvent{}
func (e *EventHandler[T]) OnDelete(obj interface{}) {
d := event.TypedDeleteEvent[T]{}
// Deal with tombstone events by pulling the object out. Tombstone events wrap the object in a
// DeleteFinalStateUnknown struct, so the object needs to be pulled out.
@ -149,7 +149,7 @@ func (e *EventHandler) OnDelete(obj interface{}) {
}
// Pull Object out of the object
if o, ok := obj.(client.Object); ok {
if o, ok := obj.(T); ok {
d.Object = o
} else {
log.Error(nil, "OnDelete missing Object",

View File

@ -4,12 +4,14 @@ import (
"context"
"errors"
"fmt"
"reflect"
"time"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
@ -17,34 +19,40 @@ import (
)
// Kind is used to provide a source of events originating inside the cluster from Watches (e.g. Pod Create).
type Kind struct {
type Kind[T client.Object] struct {
// Type is the type of object to watch. e.g. &v1.Pod{}
Type client.Object
Type T
// Cache used to watch APIs
Cache cache.Cache
// started may contain an error if one was encountered during startup. If its closed and does not
Handler handler.TypedEventHandler[T]
Predicates []predicate.TypedPredicate[T]
// startedErr may contain an error if one was encountered during startup. If its closed and does not
// contain an error, startup and syncing finished.
started chan error
startedErr chan error
startCancel func()
}
// Start is internal and should be called only by the Controller to register an EventHandler with the Informer
// to enqueue reconcile.Requests.
func (ks *Kind) Start(ctx context.Context, handler handler.EventHandler, queue workqueue.RateLimitingInterface,
prct ...predicate.Predicate) error {
if ks.Type == nil {
func (ks *Kind[T]) Start(ctx context.Context, queue workqueue.RateLimitingInterface) error {
if isNil(ks.Type) {
return fmt.Errorf("must create Kind with a non-nil object")
}
if ks.Cache == nil {
if isNil(ks.Cache) {
return fmt.Errorf("must create Kind with a non-nil cache")
}
if isNil(ks.Handler) {
return errors.New("must create Kind with non-nil handler")
}
// cache.GetInformer will block until its context is cancelled if the cache was already started and it can not
// sync that informer (most commonly due to RBAC issues).
ctx, ks.startCancel = context.WithCancel(ctx)
ks.started = make(chan error)
ks.startedErr = make(chan error)
go func() {
var (
i cache.Informer
@ -72,30 +80,30 @@ func (ks *Kind) Start(ctx context.Context, handler handler.EventHandler, queue w
return true, nil
}); err != nil {
if lastErr != nil {
ks.started <- fmt.Errorf("failed to get informer from cache: %w", lastErr)
ks.startedErr <- fmt.Errorf("failed to get informer from cache: %w", lastErr)
return
}
ks.started <- err
ks.startedErr <- err
return
}
_, err := i.AddEventHandler(NewEventHandler(ctx, queue, handler, prct).HandlerFuncs())
_, err := i.AddEventHandler(NewEventHandler(ctx, queue, ks.Handler, ks.Predicates).HandlerFuncs())
if err != nil {
ks.started <- err
ks.startedErr <- err
return
}
if !ks.Cache.WaitForCacheSync(ctx) {
// Would be great to return something more informative here
ks.started <- errors.New("cache did not sync")
ks.startedErr <- errors.New("cache did not sync")
}
close(ks.started)
close(ks.startedErr)
}()
return nil
}
func (ks *Kind) String() string {
if ks.Type != nil {
func (ks *Kind[T]) String() string {
if !isNil(ks.Type) {
return fmt.Sprintf("kind source: %T", ks.Type)
}
return "kind source: unknown type"
@ -103,9 +111,9 @@ func (ks *Kind) String() string {
// WaitForSync implements SyncingSource to allow controllers to wait with starting
// workers until the cache is synced.
func (ks *Kind) WaitForSync(ctx context.Context) error {
func (ks *Kind[T]) WaitForSync(ctx context.Context) error {
select {
case err := <-ks.started:
case err := <-ks.startedErr:
return err
case <-ctx.Done():
ks.startCancel()
@ -115,3 +123,15 @@ func (ks *Kind) WaitForSync(ctx context.Context) error {
return fmt.Errorf("timed out waiting for cache to be synced for Kind %T", ks.Type)
}
}
func isNil(arg any) bool {
if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr ||
v.Kind() == reflect.Interface ||
v.Kind() == reflect.Slice ||
v.Kind() == reflect.Map ||
v.Kind() == reflect.Chan ||
v.Kind() == reflect.Func) && v.IsNil()) {
return true
}
return false
}

View File

@ -179,6 +179,24 @@ func (cm *controllerManager) add(r Runnable) error {
return cm.runnables.Add(r)
}
// AddMetricsServerExtraHandler adds extra handler served on path to the http server that serves metrics.
func (cm *controllerManager) AddMetricsServerExtraHandler(path string, handler http.Handler) error {
cm.Lock()
defer cm.Unlock()
if cm.started {
return fmt.Errorf("unable to add new metrics handler because metrics endpoint has already been created")
}
if cm.metricsServer == nil {
cm.GetLogger().Info("warn: metrics server is currently disabled, registering extra handler %q will be ignored", path)
return nil
}
if err := cm.metricsServer.AddExtraHandler(path, handler); err != nil {
return err
}
cm.logger.V(2).Info("Registering metrics http server extra handler", "path", path)
return nil
}
// AddHealthzCheck allows you to add Healthz checker.
func (cm *controllerManager) AddHealthzCheck(name string, check healthz.Checker) error {
cm.Lock()
@ -284,9 +302,8 @@ func (cm *controllerManager) addHealthProbeServer() error {
mux.Handle(cm.livenessEndpointName+"/", http.StripPrefix(cm.livenessEndpointName, cm.healthzHandler))
}
return cm.add(&server{
Kind: "health probe",
Log: cm.logger,
return cm.add(&Server{
Name: "health probe",
Server: srv,
Listener: cm.healthProbeListener,
})
@ -302,9 +319,8 @@ func (cm *controllerManager) addPprofServer() error {
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
return cm.add(&server{
Kind: "pprof",
Log: cm.logger,
return cm.add(&Server{
Name: "pprof",
Server: srv,
Listener: cm.pprofListener,
})
@ -384,14 +400,13 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) {
}
}
// First start any internal HTTP servers, which includes health probes, metrics and profiling if enabled.
// First start any HTTP servers, which includes health probes, metrics and profiling if enabled.
//
// WARNING: Internal HTTP servers MUST start before any cache is populated, otherwise it would block
// conversion webhooks to be ready for serving which make the cache never get ready.
if err := cm.runnables.HTTPServers.Start(cm.internalCtx); err != nil {
if err != nil {
return fmt.Errorf("failed to start HTTP servers: %w", err)
}
// WARNING: HTTPServers includes the health probes, which MUST start before any cache is populated, otherwise
// it would block conversion webhooks to be ready for serving which make the cache never get ready.
logCtx := logr.NewContext(cm.internalCtx, cm.logger)
if err := cm.runnables.HTTPServers.Start(logCtx); err != nil {
return fmt.Errorf("failed to start HTTP servers: %w", err)
}
// Start any webhook servers, which includes conversion, validation, and defaulting
@ -401,23 +416,17 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) {
// between conversion webhooks and the cache sync (usually initial list) which causes the webhooks
// to never start because no cache can be populated.
if err := cm.runnables.Webhooks.Start(cm.internalCtx); err != nil {
if err != nil {
return fmt.Errorf("failed to start webhooks: %w", err)
}
return fmt.Errorf("failed to start webhooks: %w", err)
}
// Start and wait for caches.
if err := cm.runnables.Caches.Start(cm.internalCtx); err != nil {
if err != nil {
return fmt.Errorf("failed to start caches: %w", err)
}
return fmt.Errorf("failed to start caches: %w", err)
}
// Start the non-leaderelection Runnables after the cache has synced.
if err := cm.runnables.Others.Start(cm.internalCtx); err != nil {
if err != nil {
return fmt.Errorf("failed to start other runnables: %w", err)
}
return fmt.Errorf("failed to start other runnables: %w", err)
}
// Start the leader election and all required runnables.

View File

@ -22,14 +22,12 @@ import (
"fmt"
"net"
"net/http"
"reflect"
"time"
"github.com/go-logr/logr"
coordinationv1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/leaderelection/resourcelock"
@ -41,7 +39,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/cluster"
"sigs.k8s.io/controller-runtime/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/healthz"
intrec "sigs.k8s.io/controller-runtime/pkg/internal/recorder"
"sigs.k8s.io/controller-runtime/pkg/leaderelection"
@ -67,6 +64,15 @@ type Manager interface {
// election was configured.
Elected() <-chan struct{}
// AddMetricsServerExtraHandler adds an extra handler served on path to the http server that serves metrics.
// Might be useful to register some diagnostic endpoints e.g. pprof.
//
// Note that these endpoints are meant to be sensitive and shouldn't be exposed publicly.
//
// If the simple path -> handler mapping offered here is not enough,
// a new http server/listener should be added as Runnable to the manager via Add method.
AddMetricsServerExtraHandler(path string, handler http.Handler) error
// AddHealthzCheck allows you to add Healthz checker
AddHealthzCheck(name string, check healthz.Checker) error
@ -438,126 +444,6 @@ func New(config *rest.Config, options Options) (Manager, error) {
}, nil
}
// AndFrom will use a supplied type and convert to Options
// any options already set on Options will be ignored, this is used to allow
// cli flags to override anything specified in the config file.
//
// Deprecated: This function has been deprecated and will be removed in a future release,
// The Component Configuration package has been unmaintained for over a year and is no longer
// actively developed. Users should migrate to their own configuration format
// and configure Manager.Options directly.
// See https://github.com/kubernetes-sigs/controller-runtime/issues/895
// for more information, feedback, and comments.
func (o Options) AndFrom(loader config.ControllerManagerConfiguration) (Options, error) {
newObj, err := loader.Complete()
if err != nil {
return o, err
}
o = o.setLeaderElectionConfig(newObj)
if o.Cache.SyncPeriod == nil && newObj.SyncPeriod != nil {
o.Cache.SyncPeriod = &newObj.SyncPeriod.Duration
}
if len(o.Cache.DefaultNamespaces) == 0 && newObj.CacheNamespace != "" {
o.Cache.DefaultNamespaces = map[string]cache.Config{newObj.CacheNamespace: {}}
}
if o.Metrics.BindAddress == "" && newObj.Metrics.BindAddress != "" {
o.Metrics.BindAddress = newObj.Metrics.BindAddress
}
if o.HealthProbeBindAddress == "" && newObj.Health.HealthProbeBindAddress != "" {
o.HealthProbeBindAddress = newObj.Health.HealthProbeBindAddress
}
if o.ReadinessEndpointName == "" && newObj.Health.ReadinessEndpointName != "" {
o.ReadinessEndpointName = newObj.Health.ReadinessEndpointName
}
if o.LivenessEndpointName == "" && newObj.Health.LivenessEndpointName != "" {
o.LivenessEndpointName = newObj.Health.LivenessEndpointName
}
if o.WebhookServer == nil {
port := 0
if newObj.Webhook.Port != nil {
port = *newObj.Webhook.Port
}
o.WebhookServer = webhook.NewServer(webhook.Options{
Port: port,
Host: newObj.Webhook.Host,
CertDir: newObj.Webhook.CertDir,
})
}
if newObj.Controller != nil {
if o.Controller.CacheSyncTimeout == 0 && newObj.Controller.CacheSyncTimeout != nil {
o.Controller.CacheSyncTimeout = *newObj.Controller.CacheSyncTimeout
}
if len(o.Controller.GroupKindConcurrency) == 0 && len(newObj.Controller.GroupKindConcurrency) > 0 {
o.Controller.GroupKindConcurrency = newObj.Controller.GroupKindConcurrency
}
}
return o, nil
}
// AndFromOrDie will use options.AndFrom() and will panic if there are errors.
//
// Deprecated: This function has been deprecated and will be removed in a future release,
// The Component Configuration package has been unmaintained for over a year and is no longer
// actively developed. Users should migrate to their own configuration format
// and configure Manager.Options directly.
// See https://github.com/kubernetes-sigs/controller-runtime/issues/895
// for more information, feedback, and comments.
func (o Options) AndFromOrDie(loader config.ControllerManagerConfiguration) Options {
o, err := o.AndFrom(loader)
if err != nil {
panic(fmt.Sprintf("could not parse config file: %v", err))
}
return o
}
func (o Options) setLeaderElectionConfig(obj v1alpha1.ControllerManagerConfigurationSpec) Options {
if obj.LeaderElection == nil {
// The source does not have any configuration; noop
return o
}
if !o.LeaderElection && obj.LeaderElection.LeaderElect != nil {
o.LeaderElection = *obj.LeaderElection.LeaderElect
}
if o.LeaderElectionResourceLock == "" && obj.LeaderElection.ResourceLock != "" {
o.LeaderElectionResourceLock = obj.LeaderElection.ResourceLock
}
if o.LeaderElectionNamespace == "" && obj.LeaderElection.ResourceNamespace != "" {
o.LeaderElectionNamespace = obj.LeaderElection.ResourceNamespace
}
if o.LeaderElectionID == "" && obj.LeaderElection.ResourceName != "" {
o.LeaderElectionID = obj.LeaderElection.ResourceName
}
if o.LeaseDuration == nil && !reflect.DeepEqual(obj.LeaderElection.LeaseDuration, metav1.Duration{}) {
o.LeaseDuration = &obj.LeaderElection.LeaseDuration.Duration
}
if o.RenewDeadline == nil && !reflect.DeepEqual(obj.LeaderElection.RenewDeadline, metav1.Duration{}) {
o.RenewDeadline = &obj.LeaderElection.RenewDeadline.Duration
}
if o.RetryPeriod == nil && !reflect.DeepEqual(obj.LeaderElection.RetryPeriod, metav1.Duration{}) {
o.RetryPeriod = &obj.LeaderElection.RetryPeriod.Duration
}
return o
}
// defaultHealthProbeListener creates the default health probes listener bound to the given address.
func defaultHealthProbeListener(addr string) (net.Listener, error) {
if addr == "" || addr == "0" {

View File

@ -54,7 +54,10 @@ func newRunnables(baseContext BaseContextFunc, errChan chan error) *runnables {
// The runnables added after Start are started directly.
func (r *runnables) Add(fn Runnable) error {
switch runnable := fn.(type) {
case *server:
case *Server:
if runnable.NeedLeaderElection() {
return r.LeaderElection.Add(fn, nil)
}
return r.HTTPServers.Add(fn, nil)
case hasCache:
return r.Caches.Add(fn, func(ctx context.Context) bool {

View File

@ -21,34 +21,67 @@ import (
"errors"
"net"
"net/http"
"time"
"github.com/go-logr/logr"
crlog "sigs.k8s.io/controller-runtime/pkg/log"
)
// server is a general purpose HTTP server Runnable for a manager
// to serve some internal handlers such as health probes, metrics and profiling.
type server struct {
Kind string
Log logr.Logger
Server *http.Server
var (
_ Runnable = (*Server)(nil)
_ LeaderElectionRunnable = (*Server)(nil)
)
// Server is a general purpose HTTP server Runnable for a manager.
// It is used to serve some internal handlers for health probes and profiling,
// but it can also be used to run custom servers.
type Server struct {
// Name is an optional string that describes the purpose of the server. It is used in logs to distinguish
// among multiple servers.
Name string
// Server is the HTTP server to run. It is required.
Server *http.Server
// Listener is an optional listener to use. If not set, the server start a listener using the server.Addr.
// Using a listener is useful when the port reservation needs to happen in advance of this runnable starting.
Listener net.Listener
// OnlyServeWhenLeader is an optional bool that indicates that the server should only be started when the manager is the leader.
OnlyServeWhenLeader bool
// ShutdownTimeout is an optional duration that indicates how long to wait for the server to shutdown gracefully. If not set,
// the server will wait indefinitely for all connections to close.
ShutdownTimeout *time.Duration
}
func (s *server) Start(ctx context.Context) error {
log := s.Log.WithValues("kind", s.Kind, "addr", s.Listener.Addr())
// Start starts the server. It will block until the server is stopped or an error occurs.
func (s *Server) Start(ctx context.Context) error {
log := crlog.FromContext(ctx)
if s.Name != "" {
log = log.WithValues("name", s.Name)
}
log = log.WithValues("addr", s.addr())
serverShutdown := make(chan struct{})
go func() {
<-ctx.Done()
log.Info("shutting down server")
if err := s.Server.Shutdown(context.Background()); err != nil {
shutdownCtx := context.Background()
if s.ShutdownTimeout != nil {
var shutdownCancel context.CancelFunc
shutdownCtx, shutdownCancel = context.WithTimeout(context.Background(), *s.ShutdownTimeout)
defer shutdownCancel()
}
if err := s.Server.Shutdown(shutdownCtx); err != nil {
log.Error(err, "error shutting down server")
}
close(serverShutdown)
}()
log.Info("starting server")
if err := s.Server.Serve(s.Listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := s.serve(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
@ -56,6 +89,21 @@ func (s *server) Start(ctx context.Context) error {
return nil
}
func (s *server) NeedLeaderElection() bool {
return false
// NeedLeaderElection returns true if the server should only be started when the manager is the leader.
func (s *Server) NeedLeaderElection() bool {
return s.OnlyServeWhenLeader
}
func (s *Server) addr() string {
if s.Listener != nil {
return s.Listener.Addr().String()
}
return s.Server.Addr
}
func (s *Server) serve() error {
if s.Listener != nil {
return s.Server.Serve(s.Listener)
}
return s.Server.ListenAndServe()
}

View File

@ -14,6 +14,11 @@ var (
Name: "leader_election_master_status",
Help: "Gauge of if the reporting system is master of the relevant lease, 0 indicates backup, 1 indicates master. 'name' is the string used to identify the lease. Please make sure to group by name.",
}, []string{"name"})
leaderSlowpathCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "leader_election_slowpath_total",
Help: "Total number of slow path exercised in renewing leader leases. 'name' is the string used to identify the lease. Please make sure to group by name.",
}, []string{"name"})
)
func init() {
@ -23,18 +28,20 @@ func init() {
type leaderelectionMetricsProvider struct{}
func (leaderelectionMetricsProvider) NewLeaderMetric() leaderelection.SwitchMetric {
return &switchAdapter{gauge: leaderGauge}
func (leaderelectionMetricsProvider) NewLeaderMetric() leaderelection.LeaderMetric {
return leaderElectionPrometheusAdapter{}
}
type switchAdapter struct {
gauge *prometheus.GaugeVec
type leaderElectionPrometheusAdapter struct{}
func (s leaderElectionPrometheusAdapter) On(name string) {
leaderGauge.WithLabelValues(name).Set(1.0)
}
func (s *switchAdapter) On(name string) {
s.gauge.WithLabelValues(name).Set(1.0)
func (s leaderElectionPrometheusAdapter) Off(name string) {
leaderGauge.WithLabelValues(name).Set(0.0)
}
func (s *switchAdapter) Off(name string) {
s.gauge.WithLabelValues(name).Set(0.0)
func (leaderElectionPrometheusAdapter) SlowpathExercised(name string) {
leaderSlowpathCounter.WithLabelValues(name).Inc()
}

View File

@ -46,6 +46,9 @@ var DefaultBindAddress = ":8080"
// Server is a server that serves metrics.
type Server interface {
// AddExtraHandler adds extra handler served on path to the http server that serves metrics.
AddExtraHandler(path string, handler http.Handler) error
// NeedLeaderElection implements the LeaderElectionRunnable interface, which indicates
// the metrics server doesn't need leader election.
NeedLeaderElection() bool
@ -101,6 +104,9 @@ type Options struct {
// TLSOpts is used to allow configuring the TLS config used for the server.
// This also allows providing a certificate via GetCertificate.
TLSOpts []func(*tls.Config)
// ListenConfig contains options for listening to an address on the metric server.
ListenConfig net.ListenConfig
}
// Filter is a func that is added around metrics and extra handlers on the metrics server.
@ -179,6 +185,23 @@ func (*defaultServer) NeedLeaderElection() bool {
return false
}
// AddExtraHandler adds extra handler served on path to the http server that serves metrics.
func (s *defaultServer) AddExtraHandler(path string, handler http.Handler) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.options.ExtraHandlers == nil {
s.options.ExtraHandlers = make(map[string]http.Handler)
}
if path == defaultMetricsEndpoint {
return fmt.Errorf("overriding builtin %s endpoint is not allowed", defaultMetricsEndpoint)
}
if _, found := s.options.ExtraHandlers[path]; found {
return fmt.Errorf("can't register extra handler by duplicate path %q on metrics http server", path)
}
s.options.ExtraHandlers[path] = handler
return nil
}
// Start runs the server.
// It will install the metrics related resources depend on the server configuration.
func (s *defaultServer) Start(ctx context.Context) error {
@ -249,7 +272,7 @@ func (s *defaultServer) Start(ctx context.Context) error {
func (s *defaultServer) createListener(ctx context.Context, log logr.Logger) (net.Listener, error) {
if !s.options.SecureServing {
return net.Listen("tcp", s.options.BindAddress)
return s.options.ListenConfig.Listen(ctx, "tcp", s.options.BindAddress)
}
cfg := &tls.Config{ //nolint:gosec
@ -302,7 +325,12 @@ func (s *defaultServer) createListener(ctx context.Context, log logr.Logger) (ne
cfg.Certificates = []tls.Certificate{keyPair}
}
return tls.Listen("tcp", s.options.BindAddress, cfg)
l, err := s.options.ListenConfig.Listen(ctx, "tcp", s.options.BindAddress)
if err != nil {
return nil, err
}
return tls.NewListener(l, cfg), nil
}
func (s *defaultServer) GetBindAddr() string {

View File

@ -17,6 +17,7 @@ limitations under the License.
package predicate
import (
"maps"
"reflect"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -29,45 +30,51 @@ import (
var log = logf.RuntimeLog.WithName("predicate").WithName("eventFilters")
// Predicate filters events before enqueuing the keys.
type Predicate interface {
type Predicate = TypedPredicate[client.Object]
// TypedPredicate filters events before enqueuing the keys.
type TypedPredicate[T any] interface {
// Create returns true if the Create event should be processed
Create(event.CreateEvent) bool
Create(event.TypedCreateEvent[T]) bool
// Delete returns true if the Delete event should be processed
Delete(event.DeleteEvent) bool
Delete(event.TypedDeleteEvent[T]) bool
// Update returns true if the Update event should be processed
Update(event.UpdateEvent) bool
Update(event.TypedUpdateEvent[T]) bool
// Generic returns true if the Generic event should be processed
Generic(event.GenericEvent) bool
Generic(event.TypedGenericEvent[T]) bool
}
var _ Predicate = Funcs{}
var _ Predicate = ResourceVersionChangedPredicate{}
var _ Predicate = GenerationChangedPredicate{}
var _ Predicate = AnnotationChangedPredicate{}
var _ Predicate = or{}
var _ Predicate = and{}
var _ Predicate = not{}
var _ Predicate = or[client.Object]{}
var _ Predicate = and[client.Object]{}
var _ Predicate = not[client.Object]{}
// Funcs is a function that implements Predicate.
type Funcs struct {
type Funcs = TypedFuncs[client.Object]
// TypedFuncs is a function that implements TypedPredicate.
type TypedFuncs[T any] struct {
// Create returns true if the Create event should be processed
CreateFunc func(event.CreateEvent) bool
CreateFunc func(event.TypedCreateEvent[T]) bool
// Delete returns true if the Delete event should be processed
DeleteFunc func(event.DeleteEvent) bool
DeleteFunc func(event.TypedDeleteEvent[T]) bool
// Update returns true if the Update event should be processed
UpdateFunc func(event.UpdateEvent) bool
UpdateFunc func(event.TypedUpdateEvent[T]) bool
// Generic returns true if the Generic event should be processed
GenericFunc func(event.GenericEvent) bool
GenericFunc func(event.TypedGenericEvent[T]) bool
}
// Create implements Predicate.
func (p Funcs) Create(e event.CreateEvent) bool {
func (p TypedFuncs[T]) Create(e event.TypedCreateEvent[T]) bool {
if p.CreateFunc != nil {
return p.CreateFunc(e)
}
@ -75,7 +82,7 @@ func (p Funcs) Create(e event.CreateEvent) bool {
}
// Delete implements Predicate.
func (p Funcs) Delete(e event.DeleteEvent) bool {
func (p TypedFuncs[T]) Delete(e event.TypedDeleteEvent[T]) bool {
if p.DeleteFunc != nil {
return p.DeleteFunc(e)
}
@ -83,7 +90,7 @@ func (p Funcs) Delete(e event.DeleteEvent) bool {
}
// Update implements Predicate.
func (p Funcs) Update(e event.UpdateEvent) bool {
func (p TypedFuncs[T]) Update(e event.TypedUpdateEvent[T]) bool {
if p.UpdateFunc != nil {
return p.UpdateFunc(e)
}
@ -91,7 +98,7 @@ func (p Funcs) Update(e event.UpdateEvent) bool {
}
// Generic implements Predicate.
func (p Funcs) Generic(e event.GenericEvent) bool {
func (p TypedFuncs[T]) Generic(e event.TypedGenericEvent[T]) bool {
if p.GenericFunc != nil {
return p.GenericFunc(e)
}
@ -118,6 +125,26 @@ func NewPredicateFuncs(filter func(object client.Object) bool) Funcs {
}
}
// NewTypedPredicateFuncs returns a predicate funcs that applies the given filter function
// on CREATE, UPDATE, DELETE and GENERIC events. For UPDATE events, the filter is applied
// to the new object.
func NewTypedPredicateFuncs[T any](filter func(object T) bool) TypedFuncs[T] {
return TypedFuncs[T]{
CreateFunc: func(e event.TypedCreateEvent[T]) bool {
return filter(e.Object)
},
UpdateFunc: func(e event.TypedUpdateEvent[T]) bool {
return filter(e.ObjectNew)
},
DeleteFunc: func(e event.TypedDeleteEvent[T]) bool {
return filter(e.Object)
},
GenericFunc: func(e event.TypedGenericEvent[T]) bool {
return filter(e.Object)
},
}
}
// ResourceVersionChangedPredicate implements a default update predicate function on resource version change.
type ResourceVersionChangedPredicate struct {
Funcs
@ -153,17 +180,35 @@ func (ResourceVersionChangedPredicate) Update(e event.UpdateEvent) bool {
//
// * With this predicate, any update events with writes only to the status field will not be reconciled.
// So in the event that the status block is overwritten or wiped by someone else the controller will not self-correct to restore the correct status.
type GenerationChangedPredicate struct {
Funcs
type GenerationChangedPredicate = TypedGenerationChangedPredicate[client.Object]
// TypedGenerationChangedPredicate implements a default update predicate function on Generation change.
//
// This predicate will skip update events that have no change in the object's metadata.generation field.
// The metadata.generation field of an object is incremented by the API server when writes are made to the spec field of an object.
// This allows a controller to ignore update events where the spec is unchanged, and only the metadata and/or status fields are changed.
//
// For CustomResource objects the Generation is only incremented when the status subresource is enabled.
//
// Caveats:
//
// * The assumption that the Generation is incremented only on writing to the spec does not hold for all APIs.
// E.g For Deployment objects the Generation is also incremented on writes to the metadata.annotations field.
// For object types other than CustomResources be sure to verify which fields will trigger a Generation increment when they are written to.
//
// * With this predicate, any update events with writes only to the status field will not be reconciled.
// So in the event that the status block is overwritten or wiped by someone else the controller will not self-correct to restore the correct status.
type TypedGenerationChangedPredicate[T metav1.Object] struct {
TypedFuncs[T]
}
// Update implements default UpdateEvent filter for validating generation change.
func (GenerationChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil {
func (TypedGenerationChangedPredicate[T]) Update(e event.TypedUpdateEvent[T]) bool {
if isNil(e.ObjectOld) {
log.Error(nil, "Update event has no old object to update", "event", e)
return false
}
if e.ObjectNew == nil {
if isNil(e.ObjectNew) {
log.Error(nil, "Update event has no new object for update", "event", e)
return false
}
@ -183,22 +228,25 @@ func (GenerationChangedPredicate) Update(e event.UpdateEvent) bool {
//
// This is mostly useful for controllers that needs to trigger both when the resource's generation is incremented
// (i.e., when the resource' .spec changes), or an annotation changes (e.g., for a staging/alpha API).
type AnnotationChangedPredicate struct {
Funcs
type AnnotationChangedPredicate = TypedAnnotationChangedPredicate[client.Object]
// TypedAnnotationChangedPredicate implements a default update predicate function on annotation change.
type TypedAnnotationChangedPredicate[T metav1.Object] struct {
TypedFuncs[T]
}
// Update implements default UpdateEvent filter for validating annotation change.
func (AnnotationChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil {
func (TypedAnnotationChangedPredicate[T]) Update(e event.TypedUpdateEvent[T]) bool {
if isNil(e.ObjectOld) {
log.Error(nil, "Update event has no old object to update", "event", e)
return false
}
if e.ObjectNew == nil {
if isNil(e.ObjectNew) {
log.Error(nil, "Update event has no new object for update", "event", e)
return false
}
return !reflect.DeepEqual(e.ObjectNew.GetAnnotations(), e.ObjectOld.GetAnnotations())
return !maps.Equal(e.ObjectNew.GetAnnotations(), e.ObjectOld.GetAnnotations())
}
// LabelChangedPredicate implements a default update predicate function on label change.
@ -214,34 +262,37 @@ func (AnnotationChangedPredicate) Update(e event.UpdateEvent) bool {
//
// This will be helpful when object's labels is carrying some extra specification information beyond object's spec,
// and the controller will be triggered if any valid spec change (not only in spec, but also in labels) happens.
type LabelChangedPredicate struct {
Funcs
type LabelChangedPredicate = TypedLabelChangedPredicate[client.Object]
// TypedLabelChangedPredicate implements a default update predicate function on label change.
type TypedLabelChangedPredicate[T metav1.Object] struct {
TypedFuncs[T]
}
// Update implements default UpdateEvent filter for checking label change.
func (LabelChangedPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil {
func (TypedLabelChangedPredicate[T]) Update(e event.TypedUpdateEvent[T]) bool {
if isNil(e.ObjectOld) {
log.Error(nil, "Update event has no old object to update", "event", e)
return false
}
if e.ObjectNew == nil {
if isNil(e.ObjectNew) {
log.Error(nil, "Update event has no new object for update", "event", e)
return false
}
return !reflect.DeepEqual(e.ObjectNew.GetLabels(), e.ObjectOld.GetLabels())
return !maps.Equal(e.ObjectNew.GetLabels(), e.ObjectOld.GetLabels())
}
// And returns a composite predicate that implements a logical AND of the predicates passed to it.
func And(predicates ...Predicate) Predicate {
return and{predicates}
func And[T any](predicates ...TypedPredicate[T]) TypedPredicate[T] {
return and[T]{predicates}
}
type and struct {
predicates []Predicate
type and[T any] struct {
predicates []TypedPredicate[T]
}
func (a and) Create(e event.CreateEvent) bool {
func (a and[T]) Create(e event.TypedCreateEvent[T]) bool {
for _, p := range a.predicates {
if !p.Create(e) {
return false
@ -250,7 +301,7 @@ func (a and) Create(e event.CreateEvent) bool {
return true
}
func (a and) Update(e event.UpdateEvent) bool {
func (a and[T]) Update(e event.TypedUpdateEvent[T]) bool {
for _, p := range a.predicates {
if !p.Update(e) {
return false
@ -259,7 +310,7 @@ func (a and) Update(e event.UpdateEvent) bool {
return true
}
func (a and) Delete(e event.DeleteEvent) bool {
func (a and[T]) Delete(e event.TypedDeleteEvent[T]) bool {
for _, p := range a.predicates {
if !p.Delete(e) {
return false
@ -268,7 +319,7 @@ func (a and) Delete(e event.DeleteEvent) bool {
return true
}
func (a and) Generic(e event.GenericEvent) bool {
func (a and[T]) Generic(e event.TypedGenericEvent[T]) bool {
for _, p := range a.predicates {
if !p.Generic(e) {
return false
@ -278,15 +329,15 @@ func (a and) Generic(e event.GenericEvent) bool {
}
// Or returns a composite predicate that implements a logical OR of the predicates passed to it.
func Or(predicates ...Predicate) Predicate {
return or{predicates}
func Or[T any](predicates ...TypedPredicate[T]) TypedPredicate[T] {
return or[T]{predicates}
}
type or struct {
predicates []Predicate
type or[T any] struct {
predicates []TypedPredicate[T]
}
func (o or) Create(e event.CreateEvent) bool {
func (o or[T]) Create(e event.TypedCreateEvent[T]) bool {
for _, p := range o.predicates {
if p.Create(e) {
return true
@ -295,7 +346,7 @@ func (o or) Create(e event.CreateEvent) bool {
return false
}
func (o or) Update(e event.UpdateEvent) bool {
func (o or[T]) Update(e event.TypedUpdateEvent[T]) bool {
for _, p := range o.predicates {
if p.Update(e) {
return true
@ -304,7 +355,7 @@ func (o or) Update(e event.UpdateEvent) bool {
return false
}
func (o or) Delete(e event.DeleteEvent) bool {
func (o or[T]) Delete(e event.TypedDeleteEvent[T]) bool {
for _, p := range o.predicates {
if p.Delete(e) {
return true
@ -313,7 +364,7 @@ func (o or) Delete(e event.DeleteEvent) bool {
return false
}
func (o or) Generic(e event.GenericEvent) bool {
func (o or[T]) Generic(e event.TypedGenericEvent[T]) bool {
for _, p := range o.predicates {
if p.Generic(e) {
return true
@ -323,27 +374,27 @@ func (o or) Generic(e event.GenericEvent) bool {
}
// Not returns a predicate that implements a logical NOT of the predicate passed to it.
func Not(predicate Predicate) Predicate {
return not{predicate}
func Not[T any](predicate TypedPredicate[T]) TypedPredicate[T] {
return not[T]{predicate}
}
type not struct {
predicate Predicate
type not[T any] struct {
predicate TypedPredicate[T]
}
func (n not) Create(e event.CreateEvent) bool {
func (n not[T]) Create(e event.TypedCreateEvent[T]) bool {
return !n.predicate.Create(e)
}
func (n not) Update(e event.UpdateEvent) bool {
func (n not[T]) Update(e event.TypedUpdateEvent[T]) bool {
return !n.predicate.Update(e)
}
func (n not) Delete(e event.DeleteEvent) bool {
func (n not[T]) Delete(e event.TypedDeleteEvent[T]) bool {
return !n.predicate.Delete(e)
}
func (n not) Generic(e event.GenericEvent) bool {
func (n not[T]) Generic(e event.TypedGenericEvent[T]) bool {
return !n.predicate.Generic(e)
}
@ -358,3 +409,15 @@ func LabelSelectorPredicate(s metav1.LabelSelector) (Predicate, error) {
return selector.Matches(labels.Set(o.GetLabels()))
}), nil
}
func isNil(arg any) bool {
if v := reflect.ValueOf(arg); !v.IsValid() || ((v.Kind() == reflect.Ptr ||
v.Kind() == reflect.Interface ||
v.Kind() == reflect.Slice ||
v.Kind() == reflect.Map ||
v.Kind() == reflect.Chan ||
v.Kind() == reflect.Func) && v.IsNil()) {
return true
}
return false
}

View File

@ -1,93 +0,0 @@
/*
Copyright 2018 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 scheme contains utilities for gradually building Schemes,
// which contain information associating Go types with Kubernetes
// groups, versions, and kinds.
//
// Each API group should define a utility function
// called AddToScheme for adding its types to a Scheme:
//
// // in package myapigroupv1...
// var (
// SchemeGroupVersion = schema.GroupVersion{Group: "my.api.group", Version: "v1"}
// SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
// AddToScheme = SchemeBuilder.AddToScheme
// )
//
// func init() {
// SchemeBuilder.Register(&MyType{}, &MyTypeList)
// }
// var (
// scheme *runtime.Scheme = runtime.NewScheme()
// )
//
// This also true of the built-in Kubernetes types. Then, in the entrypoint for
// your manager, assemble the scheme containing exactly the types you need,
// panicing if scheme registration failed. For instance, if our controller needs
// types from the core/v1 API group (e.g. Pod), plus types from my.api.group/v1:
//
// func init() {
// utilruntime.Must(myapigroupv1.AddToScheme(scheme))
// utilruntime.Must(kubernetesscheme.AddToScheme(scheme))
// }
//
// func main() {
// mgr := controllers.NewManager(context.Background(), controllers.GetConfigOrDie(), manager.Options{
// Scheme: scheme,
// })
// // ...
// }
package scheme
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Builder builds a new Scheme for mapping go types to Kubernetes GroupVersionKinds.
type Builder struct {
GroupVersion schema.GroupVersion
runtime.SchemeBuilder
}
// Register adds one or more objects to the SchemeBuilder so they can be added to a Scheme. Register mutates bld.
func (bld *Builder) Register(object ...runtime.Object) *Builder {
bld.SchemeBuilder.Register(func(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(bld.GroupVersion, object...)
metav1.AddToGroupVersion(scheme, bld.GroupVersion)
return nil
})
return bld
}
// RegisterAll registers all types from the Builder argument. RegisterAll mutates bld.
func (bld *Builder) RegisterAll(b *Builder) *Builder {
bld.SchemeBuilder = append(bld.SchemeBuilder, b.SchemeBuilder...)
return bld
}
// AddToScheme adds all registered types to s.
func (bld *Builder) AddToScheme(s *runtime.Scheme) error {
return bld.SchemeBuilder.AddToScheme(s)
}
// Build returns a new Scheme containing the registered types.
func (bld *Builder) Build() (*runtime.Scheme, error) {
s := runtime.NewScheme()
return s, bld.AddToScheme(s)
}

View File

@ -18,10 +18,12 @@ package source
import (
"context"
"errors"
"fmt"
"sync"
"k8s.io/client-go/util/workqueue"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
@ -31,23 +33,18 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
const (
// defaultBufferSize is the default number of event notifications that can be buffered.
defaultBufferSize = 1024
)
// Source is a source of events (eh.g. Create, Update, Delete operations on Kubernetes Objects, Webhook callbacks, etc)
// Source is a source of events (e.g. Create, Update, Delete operations on Kubernetes Objects, Webhook callbacks, etc)
// which should be processed by event.EventHandlers to enqueue reconcile.Requests.
//
// * Use Kind for events originating in the cluster (e.g. Pod Create, Pod Update, Deployment Update).
//
// * Use Channel for events originating outside the cluster (eh.g. GitHub Webhook callback, Polling external urls).
// * Use Channel for events originating outside the cluster (e.g. GitHub Webhook callback, Polling external urls).
//
// Users may build their own Source implementations.
type Source interface {
// Start is internal and should be called only by the Controller to register an EventHandler with the Informer
// to enqueue reconcile.Requests.
Start(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error
Start(context.Context, workqueue.RateLimitingInterface) error
}
// SyncingSource is a source that needs syncing prior to being usable. The controller
@ -58,54 +55,92 @@ type SyncingSource interface {
}
// Kind creates a KindSource with the given cache provider.
func Kind(cache cache.Cache, object client.Object) SyncingSource {
return &internal.Kind{Type: object, Cache: cache}
func Kind[T client.Object](cache cache.Cache, object T, handler handler.TypedEventHandler[T], predicates ...predicate.TypedPredicate[T]) SyncingSource {
return &internal.Kind[T]{
Type: object,
Cache: cache,
Handler: handler,
Predicates: predicates,
}
}
var _ Source = &Channel{}
var _ Source = &channel[string]{}
// ChannelOpt allows to configure a source.Channel.
type ChannelOpt[T any] func(*channel[T])
// WithPredicates adds the configured predicates to a source.Channel.
func WithPredicates[T any](p ...predicate.TypedPredicate[T]) ChannelOpt[T] {
return func(c *channel[T]) {
c.predicates = append(c.predicates, p...)
}
}
// WithBufferSize configures the buffer size for a source.Channel. By
// default, the buffer size is 1024.
func WithBufferSize[T any](bufferSize int) ChannelOpt[T] {
return func(c *channel[T]) {
c.bufferSize = &bufferSize
}
}
// Channel is used to provide a source of events originating outside the cluster
// (e.g. GitHub Webhook callback). Channel requires the user to wire the external
// source (eh.g. http handler) to write GenericEvents to the underlying channel.
type Channel struct {
// source (e.g. http handler) to write GenericEvents to the underlying channel.
func Channel[T any](source <-chan event.TypedGenericEvent[T], handler handler.TypedEventHandler[T], opts ...ChannelOpt[T]) Source {
c := &channel[T]{
source: source,
handler: handler,
}
for _, opt := range opts {
opt(c)
}
return c
}
type channel[T any] struct {
// once ensures the event distribution goroutine will be performed only once
once sync.Once
// Source is the source channel to fetch GenericEvents
Source <-chan event.GenericEvent
// source is the source channel to fetch GenericEvents
source <-chan event.TypedGenericEvent[T]
handler handler.TypedEventHandler[T]
predicates []predicate.TypedPredicate[T]
bufferSize *int
// dest is the destination channels of the added event handlers
dest []chan event.GenericEvent
// DestBufferSize is the specified buffer size of dest channels.
// Default to 1024 if not specified.
DestBufferSize int
dest []chan event.TypedGenericEvent[T]
// destLock is to ensure the destination channels are safely added/removed
destLock sync.Mutex
}
func (cs *Channel) String() string {
func (cs *channel[T]) String() string {
return fmt.Sprintf("channel source: %p", cs)
}
// Start implements Source and should only be called by the Controller.
func (cs *Channel) Start(
func (cs *channel[T]) Start(
ctx context.Context,
handler handler.EventHandler,
queue workqueue.RateLimitingInterface,
prct ...predicate.Predicate) error {
) error {
// Source should have been specified by the user.
if cs.Source == nil {
if cs.source == nil {
return fmt.Errorf("must specify Channel.Source")
}
// use default value if DestBufferSize not specified
if cs.DestBufferSize == 0 {
cs.DestBufferSize = defaultBufferSize
if cs.handler == nil {
return errors.New("must specify Channel.Handler")
}
dst := make(chan event.GenericEvent, cs.DestBufferSize)
if cs.bufferSize == nil {
cs.bufferSize = ptr.To(1024)
}
dst := make(chan event.TypedGenericEvent[T], *cs.bufferSize)
cs.destLock.Lock()
cs.dest = append(cs.dest, dst)
@ -119,7 +154,7 @@ func (cs *Channel) Start(
go func() {
for evt := range dst {
shouldHandle := true
for _, p := range prct {
for _, p := range cs.predicates {
if !p.Generic(evt) {
shouldHandle = false
break
@ -130,7 +165,7 @@ func (cs *Channel) Start(
func() {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
handler.Generic(ctx, evt, queue)
cs.handler.Generic(ctx, evt, queue)
}()
}
}
@ -139,7 +174,7 @@ func (cs *Channel) Start(
return nil
}
func (cs *Channel) doStop() {
func (cs *channel[T]) doStop() {
cs.destLock.Lock()
defer cs.destLock.Unlock()
@ -148,7 +183,7 @@ func (cs *Channel) doStop() {
}
}
func (cs *Channel) distribute(evt event.GenericEvent) {
func (cs *channel[T]) distribute(evt event.TypedGenericEvent[T]) {
cs.destLock.Lock()
defer cs.destLock.Unlock()
@ -162,14 +197,14 @@ func (cs *Channel) distribute(evt event.GenericEvent) {
}
}
func (cs *Channel) syncLoop(ctx context.Context) {
func (cs *channel[T]) syncLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
// Close destination channels
cs.doStop()
return
case evt, stillOpen := <-cs.Source:
case evt, stillOpen := <-cs.source:
if !stillOpen {
// if the source channel is closed, we're never gonna get
// anything more on it, so stop & bail
@ -184,21 +219,25 @@ func (cs *Channel) 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 {
// Informer is the controller-runtime Informer
Informer cache.Informer
Informer cache.Informer
Handler handler.EventHandler
Predicates []predicate.Predicate
}
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, handler handler.EventHandler, queue workqueue.RateLimitingInterface,
prct ...predicate.Predicate) error {
func (is *Informer) Start(ctx context.Context, queue workqueue.RateLimitingInterface) error {
// Informer should have been specified by the user.
if is.Informer == nil {
return fmt.Errorf("must specify Informer.Informer")
}
if is.Handler == nil {
return errors.New("must specify Informer.Handler")
}
_, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, handler, prct).HandlerFuncs())
_, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, is.Handler, is.Predicates).HandlerFuncs())
if err != nil {
return err
}
@ -212,12 +251,11 @@ func (is *Informer) String() string {
var _ Source = Func(nil)
// Func is a function that implements Source.
type Func func(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error
type Func func(context.Context, workqueue.RateLimitingInterface) error
// Start implements Source.
func (f Func) Start(ctx context.Context, evt handler.EventHandler, queue workqueue.RateLimitingInterface,
pr ...predicate.Predicate) error {
return f(ctx, evt, queue, pr...)
func (f Func) Start(ctx context.Context, queue workqueue.RateLimitingInterface) error {
return f(ctx, queue)
}
func (f Func) String() string {

View File

@ -26,22 +26,35 @@ import (
// Decoder knows how to decode the contents of an admission
// request into a concrete object.
type Decoder struct {
type Decoder interface {
// Decode decodes the inlined object in the AdmissionRequest into the passed-in runtime.Object.
// If you want decode the OldObject in the AdmissionRequest, use DecodeRaw.
// It errors out if req.Object.Raw is empty i.e. containing 0 raw bytes.
Decode(req Request, into runtime.Object) error
// DecodeRaw decodes a RawExtension object into the passed-in runtime.Object.
// It errors out if rawObj is empty i.e. containing 0 raw bytes.
DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) error
}
// decoder knows how to decode the contents of an admission
// request into a concrete object.
type decoder struct {
codecs serializer.CodecFactory
}
// NewDecoder creates a Decoder given the runtime.Scheme.
func NewDecoder(scheme *runtime.Scheme) *Decoder {
// NewDecoder creates a decoder given the runtime.Scheme.
func NewDecoder(scheme *runtime.Scheme) Decoder {
if scheme == nil {
panic("scheme should never be nil")
}
return &Decoder{codecs: serializer.NewCodecFactory(scheme)}
return &decoder{codecs: serializer.NewCodecFactory(scheme)}
}
// Decode decodes the inlined object in the AdmissionRequest into the passed-in runtime.Object.
// If you want decode the OldObject in the AdmissionRequest, use DecodeRaw.
// It errors out if req.Object.Raw is empty i.e. containing 0 raw bytes.
func (d *Decoder) Decode(req Request, into runtime.Object) error {
func (d *decoder) Decode(req Request, into runtime.Object) error {
// we error out if rawObj is an empty object.
if len(req.Object.Raw) == 0 {
return fmt.Errorf("there is no content to decode")
@ -51,7 +64,7 @@ func (d *Decoder) Decode(req Request, into runtime.Object) error {
// DecodeRaw decodes a RawExtension object into the passed-in runtime.Object.
// It errors out if rawObj is empty i.e. containing 0 raw bytes.
func (d *Decoder) DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) error {
func (d *decoder) DecodeRaw(rawObj runtime.RawExtension, into runtime.Object) error {
// NB(directxman12): there's a bug/weird interaction between decoders and
// the API server where the API server doesn't send a GVK on the embedded
// objects, which means the unstructured decoder refuses to decode. It

View File

@ -43,7 +43,7 @@ func DefaultingWebhookFor(scheme *runtime.Scheme, defaulter Defaulter) *Webhook
type mutatingHandler struct {
defaulter Defaulter
decoder *Decoder
decoder Decoder
}
// Handle handles admission requests.

View File

@ -43,7 +43,7 @@ func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter C
type defaulterForType struct {
defaulter CustomDefaulter
object runtime.Object
decoder *Decoder
decoder Decoder
}
// Handle handles admission requests.

View File

@ -63,7 +63,7 @@ func ValidatingWebhookFor(scheme *runtime.Scheme, validator Validator) *Webhook
type validatingHandler struct {
validator Validator
decoder *Decoder
decoder Decoder
}
// Handle handles admission requests.

View File

@ -56,7 +56,7 @@ func WithCustomValidator(scheme *runtime.Scheme, obj runtime.Object, validator C
type validatorForType struct {
validator CustomValidator
object runtime.Object
decoder *Decoder
decoder Decoder
}
// Handle handles admission requests.